<?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: Jason T.</title>
    <description>The latest articles on DEV Community by Jason T. (@pixtidy).</description>
    <link>https://dev.to/pixtidy</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%2F4152775%2F6578c56a-ba55-4719-a5aa-a7147092a127.png</url>
      <title>DEV Community: Jason T.</title>
      <link>https://dev.to/pixtidy</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/pixtidy"/>
    <language>en</language>
    <item>
      <title>Can you reuse a passport photo? The rules for 21 documents</title>
      <dc:creator>Jason T.</dc:creator>
      <pubDate>Thu, 08 Oct 2026 16:05:01 +0000</pubDate>
      <link>https://dev.to/pixtidy/can-you-reuse-a-passport-photo-the-rules-for-21-documents-2f2n</link>
      <guid>https://dev.to/pixtidy/can-you-reuse-a-passport-photo-the-rules-for-21-documents-2f2n</guid>
      <description>&lt;p&gt;You can reuse a passport photo if it passes four checks for the new application: digital versus paper, photo age, dimensions and head ratio, and background colour. Start with the &lt;a href="https://pixtidy.com/photo-requirements?utm_source=devto&amp;amp;utm_campaign=devto-reuse" rel="noopener noreferrer"&gt;21-document requirements table and its official source links&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Some failures can be fixed by resizing or recompressing; others cannot. A crop that is too tight needs the original, wider image.&lt;/p&gt;

&lt;h2&gt;
  
  
  Do you need a digital file or a paper photo?
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Check what the application accepts before editing your photo.&lt;/strong&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;DS-160 and Thailand e-Visa require a digital file. A print works only where paper photos are accepted. If you have the original digital photo from your studio or booth, start there.&lt;/p&gt;

&lt;p&gt;These documents have restrictions on scans or photos of photos:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Document&lt;/th&gt;
&lt;th&gt;Restriction&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;US passport&lt;/td&gt;
&lt;td&gt;No photocopies or digitally scanned photos&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;New Zealand passport&lt;/td&gt;
&lt;td&gt;Scans of printed photos are not accepted&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;UK visa&lt;/td&gt;
&lt;td&gt;Not a scan or photo of another photo&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;NZeTA&lt;/td&gt;
&lt;td&gt;Not a photo of a photo&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;For the UK visa, there is a further restriction: you cannot use the same photo that is in your passport or identity card.&lt;/p&gt;

&lt;p&gt;Before working on upload limits, establish whether you have an acceptable source image.&lt;/p&gt;

&lt;h2&gt;
  
  
  Is your passport photo still recent enough?
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Check the photo age requirement for your document.&lt;/strong&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Document&lt;/th&gt;
&lt;th&gt;Photo age requirement&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;US passport&lt;/td&gt;
&lt;td&gt;Taken in the last 6 months&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;New Zealand passport&lt;/td&gt;
&lt;td&gt;Less than 6 months old&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;NZeTA&lt;/td&gt;
&lt;td&gt;Less than 6 months old&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Pakistan visa&lt;/td&gt;
&lt;td&gt;No more than 6 months old&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Ethiopia e-Visa&lt;/td&gt;
&lt;td&gt;Taken within the last six months&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;a href="https://pixtidy.com/photo-requirements?utm_source=devto&amp;amp;utm_campaign=devto-reuse" rel="noopener noreferrer"&gt;Look up these requirements and their official sources&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  What dimensions and head size does your application require?
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Check shape, dimensions, file size and framing separately.&lt;/strong&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;"Passport size" is too vague for this decision. Use the requirements for the document you are applying for:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Document&lt;/th&gt;
&lt;th&gt;Dimensions and format&lt;/th&gt;
&lt;th&gt;File size&lt;/th&gt;
&lt;th&gt;Head size&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;India e-Visa&lt;/td&gt;
&lt;td&gt;Square JPEG&lt;/td&gt;
&lt;td&gt;10 KB–1 MB&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;OCI&lt;/td&gt;
&lt;td&gt;Square JPEG, 200×200 to 900×900 px&lt;/td&gt;
&lt;td&gt;Maximum 200 KB&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;DS-160&lt;/td&gt;
&lt;td&gt;Square, 600×600 to 1200×1200 px&lt;/td&gt;
&lt;td&gt;Maximum 240 KB&lt;/td&gt;
&lt;td&gt;50–69% of image height&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Schengen&lt;/td&gt;
&lt;td&gt;35×45 mm&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;70–80% (32–36 mm)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;UK visa/ETA&lt;/td&gt;
&lt;td&gt;At least 600×750 px, JPG&lt;/td&gt;
&lt;td&gt;50 KB–6 MB&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;a href="https://pixtidy.com/photo-requirements?utm_source=devto&amp;amp;utm_campaign=devto-reuse" rel="noopener noreferrer"&gt;Find the document rows and official source links for these numerical requirements&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;For DS-160, a square file must also pass the pixel range, maximum file size and head-height check.&lt;/p&gt;

&lt;p&gt;Keep the units separate, too. Schengen's 35×45 mm describes print dimensions. DS-160's dimensions are in pixels.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Resizing and reframing solve different problems.&lt;/strong&gt; If your head occupies 73% of the image height, it exceeds DS-160's 69% maximum.&lt;/p&gt;

&lt;p&gt;You need the original, wider photo to make a suitable crop.&lt;/p&gt;

&lt;h2&gt;
  
  
  Does your existing background colour work?
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Compare the background with the destination document's rule.&lt;/strong&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Document&lt;/th&gt;
&lt;th&gt;Background requirement&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;OCI&lt;/td&gt;
&lt;td&gt;Plain light-coloured, not white&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Australia visa&lt;/td&gt;
&lt;td&gt;Plain neutral or light grey&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Vietnam e-Visa&lt;/td&gt;
&lt;td&gt;White&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;a href="https://pixtidy.com/photo-requirements?utm_source=devto&amp;amp;utm_campaign=devto-reuse" rel="noopener noreferrer"&gt;Check the document requirements and their official sources&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;A typical white-background passport photo fails OCI's background requirement.&lt;/p&gt;

&lt;p&gt;Vietnam e-Visa requires white.&lt;/p&gt;

&lt;h2&gt;
  
  
  What can a JavaScript validator actually check?
&lt;/h2&gt;

&lt;p&gt;You can automate the square and byte-window checks. This function takes width, height and file size in bytes, then checks India's square requirement and a 10 KB–1 MB window.&lt;/p&gt;

&lt;p&gt;The implementation uses decimal units: 1 KB is 1,000 bytes and 1 MB is 1,000,000 bytes. That is an explicit code convention, not an additional photo requirement.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;checkIndiaPhotoBasics&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="nx"&gt;fileSizeBytes&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;validDimensions&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;
    &lt;span class="nb"&gt;Number&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;isInteger&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="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt;
    &lt;span class="nb"&gt;Number&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;isInteger&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="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt;
    &lt;span class="nx"&gt;width&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt;
    &lt;span class="nx"&gt;height&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;0&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;square&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;validDimensions&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&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;height&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="c1"&gt;// Decimal-unit convention for this validator.&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;minBytes&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;1000&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;maxBytes&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;1000&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;1000&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;sizeInRange&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;
    &lt;span class="nb"&gt;Number&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;isInteger&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;fileSizeBytes&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt;
    &lt;span class="nx"&gt;fileSizeBytes&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="nx"&gt;minBytes&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt;
    &lt;span class="nx"&gt;fileSizeBytes&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="nx"&gt;maxBytes&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;square&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nx"&gt;sizeInRange&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;passesBasicChecks&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;square&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;sizeInRange&lt;/span&gt;
  &lt;span class="p"&gt;};&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The function deliberately returns &lt;code&gt;passesBasicChecks&lt;/code&gt;, not &lt;code&gt;validPassportPhoto&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Which edge cases can still rule out reuse?
&lt;/h2&gt;

&lt;p&gt;Before submitting, watch for these exceptions:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;UK visa:&lt;/strong&gt; The photo must not be a scan or photo of another photo, or the same photo that is in your passport or identity card.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;DS-160:&lt;/strong&gt; A tightly cropped photo with a head height of 73% exceeds the 69% maximum and needs the original, wider photo. Return to the wider original rather than repeatedly resizing the crop.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;A white background can be the wrong background.&lt;/strong&gt; For OCI, check "not white" before spending time adjusting file size.&lt;/p&gt;

&lt;p&gt;Meeting the rules does not guarantee acceptance.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where can you look up all 21 documents?
&lt;/h2&gt;

&lt;p&gt;Keep the &lt;a href="https://pixtidy.com/photo-requirements?utm_source=devto&amp;amp;utm_campaign=devto-reuse" rel="noopener noreferrer"&gt;21-document requirements table&lt;/a&gt; beside your application.&lt;/p&gt;

&lt;p&gt;For more help deciding whether to reuse your file, use the &lt;a href="https://pixtidy.com/guides/use-existing-passport-photo?utm_source=devto&amp;amp;utm_campaign=devto-reuse" rel="noopener noreferrer"&gt;existing-passport-photo guide&lt;/a&gt;. You can also use the &lt;a href="https://pixtidy.com/passport-photo-checker?utm_source=devto&amp;amp;utm_campaign=devto-reuse" rel="noopener noreferrer"&gt;free photo checker before paying&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Related: &lt;a href="https://dev.to/pixtidy/convert-photo-sizes-from-millimetres-to-pixels-without-mixing-up-upload-rules-1ig0"&gt;Convert photo sizes from millimetres to pixels without mixing up upload rules&lt;/a&gt; · &lt;a href="https://dev.to/pixtidy/how-i-built-pixtidys-browser-layout-for-six-2x2-inch-green-card-photos-on-a-4x6-print-2e3h"&gt;How I built pixtidy's browser layout for six 2 2-inch green card photos on a 4 6 print&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Disclosure: I built pixtidy.&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>javascript</category>
      <category>travel</category>
      <category>beginners</category>
    </item>
    <item>
      <title>Convert photo sizes from millimetres to pixels without mixing up upload rules</title>
      <dc:creator>Jason T.</dc:creator>
      <pubDate>Wed, 07 Oct 2026 16:05:01 +0000</pubDate>
      <link>https://dev.to/pixtidy/convert-photo-sizes-from-millimetres-to-pixels-without-mixing-up-upload-rules-1ig0</link>
      <guid>https://dev.to/pixtidy/convert-photo-sizes-from-millimetres-to-pixels-without-mixing-up-upload-rules-1ig0</guid>
      <description>&lt;p&gt;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 &lt;code&gt;Math.round(mm / 25.4 * dpi)&lt;/code&gt; for print conversions. Validate upload dimensions and head height against their own rules.&lt;/p&gt;

&lt;p&gt;The official print and pixel sizes for all 21 documents are in &lt;a href="https://pixtidy.com/photo-requirements?utm_source=devto&amp;amp;utm_campaign=devto-mmpx" rel="noopener noreferrer"&gt;pixtidy's requirements table&lt;/a&gt; (disclosure: I built it).&lt;/p&gt;

&lt;h2&gt;
  
  
  How do you convert millimetres to pixels in JavaScript?
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;Pass the physical measurement and dpi explicitly.
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;mmToPixels&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="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="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="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="nf"&gt;mmToPixels&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;33&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;300&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// 390&lt;/span&gt;
&lt;span class="nf"&gt;mmToPixels&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;48&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;300&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// 567&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Apply the function independently to width and height. Round at the end, not during the division.&lt;/p&gt;

&lt;p&gt;Keep this function narrow. Giving it file-format or head-height logic would hide which requirement you are actually testing.&lt;/p&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;p&gt;That one-pixel difference is a useful regression test for code that generates multiple print resolutions.&lt;/p&gt;

&lt;h2&gt;
  
  
  What sizes should your conversion tests expect?
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;Keep the physical input beside each expected result.&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Print photo&lt;/th&gt;
&lt;th&gt;Physical size&lt;/th&gt;
&lt;th&gt;Dpi&lt;/th&gt;
&lt;th&gt;Converted size&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Schengen visa&lt;/td&gt;
&lt;td&gt;35 × 45 mm&lt;/td&gt;
&lt;td&gt;300&lt;/td&gt;
&lt;td&gt;413 × 531 px&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Schengen visa&lt;/td&gt;
&lt;td&gt;35 × 45 mm&lt;/td&gt;
&lt;td&gt;600&lt;/td&gt;
&lt;td&gt;827 × 1063 px&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;US passport&lt;/td&gt;
&lt;td&gt;2 × 2 in&lt;/td&gt;
&lt;td&gt;300&lt;/td&gt;
&lt;td&gt;600 × 600 px&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;US passport&lt;/td&gt;
&lt;td&gt;2 × 2 in&lt;/td&gt;
&lt;td&gt;600&lt;/td&gt;
&lt;td&gt;1200 × 1200 px&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Chinese visa&lt;/td&gt;
&lt;td&gt;33 × 48 mm&lt;/td&gt;
&lt;td&gt;300&lt;/td&gt;
&lt;td&gt;390 × 567 px&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Use the millimetre rows as fixtures for &lt;code&gt;mmToPixels&lt;/code&gt;. The US passport print size is specified in inches: 2 × 2 in.&lt;/p&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;h2&gt;
  
  
  How should you model print, upload, and head-height requirements?
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;Give each requirement its own scope and unit.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;A compact model can preserve the original measurements without turning every rule into pixels up front:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;requirements&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;chinaVisa&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;print&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;unit&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;mm&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;width&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;33&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;height&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;48&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="na"&gt;upload&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;widthPx&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;354&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;420&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
      &lt;span class="na"&gt;heightPx&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;472&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;560&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
      &lt;span class="na"&gt;format&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;JPEG&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;sizeKB&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;40&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;120&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="na"&gt;head&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;scope&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;print&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;unit&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;mm&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;range&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;28&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;33&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="na"&gt;schengenVisa&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;print&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;unit&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;mm&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;width&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;35&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;height&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;45&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="na"&gt;head&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;scope&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;print&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;unit&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;mm&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;range&lt;/span&gt;&lt;span class="p"&gt;:&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="mi"&gt;36&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
      &lt;span class="na"&gt;measurement&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;chin-to-crown&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="na"&gt;usPassport&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;print&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;unit&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;in&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;width&lt;/span&gt;&lt;span class="p"&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;height&lt;/span&gt;&lt;span class="p"&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="na"&gt;ds160&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;upload&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;widthPx&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;600&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1200&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
      &lt;span class="na"&gt;heightPx&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;600&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1200&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
      &lt;span class="na"&gt;square&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;sizeKB&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;240&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="na"&gt;head&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;scope&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;digital&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;unit&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;percent&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;range&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;50&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;69&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="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The DS-160 file-size rule is 240 KB or less. China's digital file-size rule is 40–120 KB.&lt;/p&gt;

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

&lt;p&gt;The &lt;code&gt;scope&lt;/code&gt; field matters for China's head-height requirement. Its 28–33 mm measurement belongs to the 48 mm paper photo.&lt;/p&gt;

&lt;h2&gt;
  
  
  How can you validate digital dimensions and file limits?
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;Return individual failures instead of a single unexplained boolean.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;This validator takes normalized metadata. &lt;code&gt;sizeKB&lt;/code&gt; is already expressed in KB, and &lt;code&gt;format&lt;/code&gt; uses the model's &lt;code&gt;"JPEG"&lt;/code&gt; label.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;validateUpload&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;rule&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;photo&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;errors&lt;/span&gt; &lt;span class="o"&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;within&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;min&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;max&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt;
    &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;min&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nx"&gt;value&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="nx"&gt;min&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;value&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="nx"&gt;max&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nf"&gt;within&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;photo&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;widthPx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;rule&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;widthPx&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;errors&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;push&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;width&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nf"&gt;within&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;photo&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;heightPx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;rule&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;heightPx&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;errors&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;push&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;height&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;rule&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;square&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;photo&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;widthPx&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="nx"&gt;photo&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;heightPx&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;errors&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;push&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;square&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;rule&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;format&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;photo&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;format&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="nx"&gt;rule&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;format&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;errors&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;push&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;format&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nf"&gt;within&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;photo&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;sizeKB&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;rule&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;sizeKB&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;errors&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;push&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;file-size&lt;/span&gt;&lt;span class="dl"&gt;"&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="nx"&gt;errors&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;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.&lt;/p&gt;

&lt;p&gt;DS-160 has a 240 KB ceiling.&lt;/p&gt;

&lt;p&gt;Treat these results as metadata checks, not a promise of acceptance. Head height still needs its own measurement.&lt;/p&gt;

&lt;h2&gt;
  
  
  Which boundary tests catch the China mismatch?
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;Test rejected inputs as well as the exact allowed boundaries.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Run this after the function and model above. Each test throws if the validator returns an unexpected result.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;expectErrors&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;rule&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;photo&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;expected&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;actual&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;validateUpload&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;rule&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;photo&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;actual&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;expected&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;: got &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;actual&lt;/span&gt;&lt;span class="p"&gt;)}&lt;/span&gt;&lt;span class="s2"&gt;`&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;china&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;requirements&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;chinaVisa&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;upload&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;base&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;widthPx&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;390&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;heightPx&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;560&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;format&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;JPEG&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;sizeKB&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;40&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;cases&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
  &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;minimum dimensions&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;widthPx&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;354&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;heightPx&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;472&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="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;maximum dimensions&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;widthPx&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;420&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;heightPx&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;560&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="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;width below minimum&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;widthPx&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;353&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;width&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]],&lt;/span&gt;
  &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;width above maximum&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;widthPx&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;421&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;width&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]],&lt;/span&gt;
  &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;height below minimum&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;heightPx&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;471&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;height&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]],&lt;/span&gt;
  &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;height above maximum&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;heightPx&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;561&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;height&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]],&lt;/span&gt;
  &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;print conversion&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;widthPx&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;mmToPixels&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;33&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;300&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="na"&gt;heightPx&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;mmToPixels&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;48&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;300&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="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;height&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]],&lt;/span&gt;
  &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;below 40 KB&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;sizeKB&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;39&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;file-size&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]],&lt;/span&gt;
  &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;at 40 KB&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;sizeKB&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;40&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="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;at 120 KB&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;sizeKB&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;120&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="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;above 120 KB&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;sizeKB&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;121&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;file-size&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]],&lt;/span&gt;
  &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;wrong format&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;format&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;PNG&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;format&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]]&lt;/span&gt;
&lt;span class="p"&gt;];&lt;/span&gt;

&lt;span class="k"&gt;for &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;patch&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;expected&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="nx"&gt;cases&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nf"&gt;expectErrors&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;china&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="nx"&gt;base&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;...&lt;/span&gt;&lt;span class="nx"&gt;patch&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="nx"&gt;expected&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;ds160&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;requirements&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ds160&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;upload&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;for &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;sizeKB&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;expected&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
  &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;240&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="mi"&gt;241&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;file-size&lt;/span&gt;&lt;span class="dl"&gt;"&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="nf"&gt;expectErrors&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`DS-160 &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;sizeKB&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; KB`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;ds160&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;widthPx&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;600&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;heightPx&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;600&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nx"&gt;sizeKB&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="nx"&gt;expected&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;These tests make the inclusive boundaries visible.&lt;/p&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;h2&gt;
  
  
  How do you test head height without mixing units?
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;Schengen specifies head height in millimetres; DS-160 specifies a percentage of image height.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;For Schengen, chin-to-crown height is 32–36 mm. At 300 dpi, the rounded range is 378–425 px.&lt;/p&gt;

&lt;p&gt;DS-160 instead requires 50–69% of image height:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;schengenHead300&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;
  &lt;span class="nx"&gt;requirements&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;schengenVisa&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;head&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;range&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;map&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;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;mmToPixels&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="mi"&gt;300&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// [378, 425]&lt;/span&gt;

&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;ds160HeadPasses&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;headPx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;imageHeightPx&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;percent&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;headPx&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="nx"&gt;imageHeightPx&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;min&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;max&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;requirements&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ds160&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;head&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;range&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;percent&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="nx"&gt;min&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;percent&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="nx"&gt;max&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;China's JPEG requirement and both file-size bounds remain separate.&lt;/p&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where can you find document-specific official sources?
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;Review the document's source before updating its fixture.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;a href="https://pixtidy.com/photo-requirements?utm_source=devto&amp;amp;utm_campaign=devto-mmpx" rel="noopener noreferrer"&gt;pixtidy's requirements table&lt;/a&gt; lists print sizes, digital file rules, head height, and official-source links for 21 documents.&lt;/p&gt;

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

&lt;p&gt;Related: &lt;a href="https://dev.to/pixtidy/schengen-visa-photos-are-35x45-mm-turning-millimetre-rules-into-browser-checks-1d74"&gt;Schengen visa photos are 35 45 mm: turning millimetre rules into browser checks&lt;/a&gt; · &lt;a href="https://dev.to/pixtidy/why-a-ds-160-photo-can-be-too-small-and-how-i-test-it-in-the-browser-5bod"&gt;Why a DS-160 photo can be too small, and how I test it in the browser&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Disclosure: I built pixtidy.&lt;/p&gt;

</description>
      <category>javascript</category>
      <category>webdev</category>
      <category>testing</category>
      <category>images</category>
    </item>
    <item>
      <title>How I built pixtidy's browser layout for six 2 2-inch green card photos on a 4 6 print</title>
      <dc:creator>Jason T.</dc:creator>
      <pubDate>Tue, 06 Oct 2026 16:05:01 +0000</pubDate>
      <link>https://dev.to/pixtidy/how-i-built-pixtidys-browser-layout-for-six-2x2-inch-green-card-photos-on-a-4x6-print-2e3h</link>
      <guid>https://dev.to/pixtidy/how-i-built-pixtidys-browser-layout-for-six-2x2-inch-green-card-photos-on-a-4x6-print-2e3h</guid>
      <description>&lt;p&gt;USCIS asks for two identical colour 2×2-inch photos. I built pixtidy's browser layout to fit six on one 4×6-inch print: an 1800×1200-pixel sheet at 300 dpi. Below, I explain the layout function and how to draw the sheet on a canvas for JPEG export. Print at actual size to preserve the dimensions.&lt;/p&gt;

&lt;p&gt;You can check your own photo against these rules for free in your browser on &lt;a href="https://pixtidy.com/us-green-card-photo?utm_source=devto&amp;amp;utm_campaign=devto-gc-sheet" rel="noopener noreferrer"&gt;this page&lt;/a&gt;; the photo is never uploaded (disclosure: I built it).&lt;/p&gt;

&lt;h2&gt;
  
  
  What are the USCIS green card photo requirements?
&lt;/h2&gt;

&lt;p&gt;Start with a photo that meets the requirements before you repeat it.&lt;/p&gt;

&lt;p&gt;You need &lt;strong&gt;two identical colour passport-style photos taken recently&lt;/strong&gt;, each:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;2×2 inches.&lt;/li&gt;
&lt;li&gt;Printed on thin paper with a glossy finish.&lt;/li&gt;
&lt;li&gt;Unmounted and unretouched.&lt;/li&gt;
&lt;li&gt;Taken against a white to off-white background.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The head, measured from the top of the hair to the bottom of the chin, must be &lt;strong&gt;1 to 1⅜ inches&lt;/strong&gt;. The eyes must be &lt;strong&gt;1⅛ to 1⅜ inches from the bottom of the photo&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Lightly print your name and A-Number, if any, on the back with a pencil or felt pen. The same photos are used for Forms I-485, I-765 and I-131.&lt;/p&gt;

&lt;p&gt;Meeting the rules does not guarantee acceptance.&lt;/p&gt;

&lt;h2&gt;
  
  
  How do six 2×2-inch photos fit on a 4×6-inch print?
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;1. Use a horizontal sheet with no space between photos.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;At 300 dpi, the dimensions are:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Item&lt;/th&gt;
&lt;th&gt;Printed dimensions&lt;/th&gt;
&lt;th&gt;Pixel dimensions&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Sheet&lt;/td&gt;
&lt;td&gt;6 inches wide × 4 inches high&lt;/td&gt;
&lt;td&gt;1800×1200&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Each photo&lt;/td&gt;
&lt;td&gt;2 inches wide × 2 inches high&lt;/td&gt;
&lt;td&gt;600×600&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Three photos fill the width, and two fill the height. One print therefore gives you the required pair and four spares.&lt;/p&gt;

&lt;p&gt;The layout function calculates the columns and rows with:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;columns = floor((sheet width + gap) / (photo width + gap))
rows    = floor((sheet height + gap) / (photo height + gap))
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For this sheet, &lt;code&gt;gapPx = 0&lt;/code&gt;, so the result is three columns and two rows. The grid fills the canvas, and its centred starting position is &lt;code&gt;(0, 0)&lt;/code&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Top-left coordinates, in pixels:

(0, 0)       (600, 0)       (1200, 0)
(0, 600)     (600, 600)     (1200, 600)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The internal cut boundaries sit at &lt;code&gt;x = 600&lt;/code&gt;, &lt;code&gt;x = 1200&lt;/code&gt; and &lt;code&gt;y = 600&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  How does the layout function work?
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;2. Calculate the slots for six copies of the photo.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;This is my layout function from &lt;code&gt;web/src/lib/photo/sheet.ts&lt;/code&gt;. It calculates how many photos fit and centres the resulting grid on the sheet.&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="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;layoutSheet&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;sheet&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;photo&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;gapPx&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&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;cols&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;floor&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;sheet&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;widthPx&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;gapPx&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;photo&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;widthPx&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;gapPx&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;floor&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;sheet&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;heightPx&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;gapPx&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;photo&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;heightPx&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;gapPx&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;gridW&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;cols&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nx"&gt;photo&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;widthPx&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;cols&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;gapPx&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;gridH&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;photo&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;heightPx&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;gapPx&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;x0&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;sheet&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;widthPx&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nx"&gt;gridW&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;y0&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;sheet&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;heightPx&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nx"&gt;gridH&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;slots&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[];&lt;/span&gt;
  &lt;span class="k"&gt;for &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;r&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nx"&gt;r&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="nx"&gt;rows&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nx"&gt;r&lt;/span&gt;&lt;span class="o"&gt;++&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;for &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;c&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nx"&gt;c&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="nx"&gt;cols&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nx"&gt;c&lt;/span&gt;&lt;span class="o"&gt;++&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="nx"&gt;slots&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;push&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="nx"&gt;x0&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;c&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;photo&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;widthPx&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;gapPx&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="nx"&gt;y0&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;r&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;photo&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;heightPx&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;gapPx&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="nx"&gt;slots&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For the worked example, the sheet dimensions are &lt;code&gt;1800×1200&lt;/code&gt;, the photo dimensions are &lt;code&gt;600×600&lt;/code&gt;, and the gap is &lt;code&gt;0&lt;/code&gt;. The function returns the six positions shown above.&lt;/p&gt;

&lt;h2&gt;
  
  
  How do you draw the sheet on a canvas?
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;3. Fill the canvas white, then draw the same photo in each slot.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Use an 1800×1200-pixel canvas for the sheet. After filling it white, call &lt;code&gt;drawImage&lt;/code&gt; for each returned slot, drawing the same photo at 600×600 pixels.&lt;/p&gt;

&lt;p&gt;The arrangement is:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;                  6 inches
        ┌─────────┬─────────┬─────────┐
        │         │         │         │
        │  Same   │  Same   │  Same   │
        │  photo  │  photo  │  photo  │
        │         │         │         │
4 inches├─────────┼─────────┼─────────┤
        │         │         │         │
        │  Same   │  Same   │  Same   │
        │  photo  │  photo  │  photo  │
        │         │         │         │
        └─────────┴─────────┴─────────┘
             Each cell: 2×2 inches
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;strong&gt;1-pixel light-grey cut guides are my design choice&lt;/strong&gt;, not a USCIS rule. Draw them on the canvas before exporting the sheet as a JPEG so they appear in the exported image.&lt;/p&gt;

&lt;h2&gt;
  
  
  How do you print the JPEG at the correct size?
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;4. Export the JPEG and set its physical print size explicitly.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Use this compact checklist:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;[ ] Print the sheet &lt;strong&gt;6 inches wide × 4 inches high&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;[ ] Turn off &lt;strong&gt;"fit to page"&lt;/strong&gt; scaling.&lt;/li&gt;
&lt;li&gt;[ ] Use &lt;strong&gt;thin paper with a glossy finish&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;[ ] Check that each printed photo measures &lt;strong&gt;2×2 inches&lt;/strong&gt; before using it.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The intended conversion is &lt;code&gt;600 pixels ÷ 300 pixels per inch = 2 inches&lt;/code&gt;. The export provides the pixels; the print settings must preserve that physical relationship.&lt;/p&gt;

&lt;h2&gt;
  
  
  Which edge cases need a separate check?
&lt;/h2&gt;

&lt;p&gt;The layout function accepts a gap. The zero-gap grid already occupies the entire sheet.&lt;/p&gt;

&lt;p&gt;Check the photo requirements before making the sheet.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where can you use pixtidy's green card photo tools?
&lt;/h2&gt;

&lt;p&gt;You can use &lt;a href="https://pixtidy.com/us-green-card-photo?utm_source=devto&amp;amp;utm_campaign=devto-gc-sheet" rel="noopener noreferrer"&gt;pixtidy's green card photo page&lt;/a&gt; for the green card workflow and &lt;a href="https://pixtidy.com/photo-requirements?utm_source=devto&amp;amp;utm_campaign=devto-gc-sheet" rel="noopener noreferrer"&gt;pixtidy's photo requirements page&lt;/a&gt; to review the requirements.&lt;/p&gt;

&lt;p&gt;Related: &lt;a href="https://dev.to/pixtidy/why-a-ready-made-id-photo-failed-our-browser-framing-check-3086"&gt;Why a ready-made ID photo failed our browser framing check&lt;/a&gt; · &lt;a href="https://dev.to/pixtidy/schengen-visa-photos-are-35x45-mm-turning-millimetre-rules-into-browser-checks-1d74"&gt;Schengen visa photos are 35 45 mm: turning millimetre rules into browser checks&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Disclosure: I built pixtidy.&lt;/p&gt;

</description>
      <category>javascript</category>
      <category>webdev</category>
      <category>canvas</category>
      <category>typescript</category>
    </item>
    <item>
      <title>ChatGPT sent French users to our English page: what we changed</title>
      <dc:creator>Jason T.</dc:creator>
      <pubDate>Mon, 05 Oct 2026 16:05:00 +0000</pubDate>
      <link>https://dev.to/pixtidy/chatgpt-sent-french-users-to-our-english-page-what-we-changed-enh</link>
      <guid>https://dev.to/pixtidy/chatgpt-sent-french-users-to-our-english-page-what-we-changed-enh</guid>
      <description>&lt;p&gt;Two ChatGPT-tagged visits from France exposed a missing French Thailand e-Visa photo page. We added that page, localized links in &lt;code&gt;llms.txt&lt;/code&gt;, and a hydration-safe browser-language hint.&lt;/p&gt;

&lt;h2&gt;
  
  
  What did the French visits actually tell us?
&lt;/h2&gt;

&lt;p&gt;On 2026-10-03, two visitors reached pixtidy's English Thailand e-Visa photo page with &lt;code&gt;utm_source=chatgpt.com&lt;/code&gt;. Earlier that evening, a third French visitor had reached the same page.&lt;/p&gt;

&lt;p&gt;At first, our analytics obscured this. PostHog's own GeoIP located our forwarding server in Ashburn, Virginia, producing a country breakdown that said "almost all US." That was the server's location, not a useful account of our audience.&lt;/p&gt;

&lt;p&gt;The country from the request header on our own event endpoint placed these visitors in France.&lt;/p&gt;

&lt;p&gt;That gave us a concrete gap to investigate: we had an English Thailand page and no French equivalent. Country helped expose the missing page; browser-language preferences would decide whether to offer its link.&lt;/p&gt;

&lt;h2&gt;
  
  
  What should you change when a localized page is missing?
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;We added the missing French page.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;We added &lt;a href="https://pixtidy.com/fr/photo-evisa-thailande?utm_source=devto&amp;amp;utm_campaign=devto-chatgpt-locale" rel="noopener noreferrer"&gt;the French Thailand e-Visa photo page&lt;/a&gt; at &lt;code&gt;/fr/photo-evisa-thailande&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;A writing tool drafted the copy, and a second model reviewed it. One required correction: the lead repeated the meta description word for word. We removed that repetition.&lt;/p&gt;

&lt;p&gt;The page's reviewed title also became the text for the language hint. That kept the hint tied to the actual destination rather than introducing a separate, unreviewed translation.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Connect the localized page to the document listing.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Each document line in &lt;a href="https://pixtidy.com/llms.txt" rel="noopener noreferrer"&gt;our llms.txt&lt;/a&gt; now ends with an &lt;code&gt;Also in:&lt;/code&gt; suffix listing localized pages.&lt;/p&gt;

&lt;p&gt;For the Thailand line, the compact before-and-after is:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Before: no French-page suffix.&lt;/li&gt;
&lt;li&gt;After: an &lt;code&gt;Also in:&lt;/code&gt; suffix linking to the French page.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That shows the added relationship without reproducing the rest of the document line. A unit test checks that the Thailand line links to the French page.&lt;/p&gt;

&lt;p&gt;This test answers a narrow question: did we retain the link?&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Offer a link beneath the English page's H1.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;English document pages now read &lt;code&gt;navigator.languages&lt;/code&gt;. If the first non-English language ranks above English and the corresponding page exists, the hint appears.&lt;/p&gt;

&lt;p&gt;In our browser checks, French showed the link and English showed nothing.&lt;/p&gt;

&lt;h2&gt;
  
  
  How do you wire the hint with useSyncExternalStore?
&lt;/h2&gt;

&lt;p&gt;Our implementation uses &lt;code&gt;useSyncExternalStore&lt;/code&gt; so the server-rendered hint stays empty, with no hydration mismatch.&lt;/p&gt;

&lt;p&gt;Here is simplified wiring that includes the language selection, server snapshot, and destination check. The &lt;code&gt;pages&lt;/code&gt; prop represents the current document's localized destinations, keyed by language, with each destination's &lt;code&gt;href&lt;/code&gt; and reviewed &lt;code&gt;title&lt;/code&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight jsx"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;useSyncExternalStore&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;react&lt;/span&gt;&lt;span class="dl"&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;subscribe&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;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;getServerSnapshot&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;getSnapshot&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;codes&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;navigator&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;languages&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;tag&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt;
    &lt;span class="nx"&gt;tag&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;toLowerCase&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;split&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;-&lt;/span&gt;&lt;span class="dl"&gt;"&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="p"&gt;);&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;other&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;codes&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;findIndex&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;code&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;code&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;en&lt;/span&gt;&lt;span class="dl"&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;english&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;codes&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;indexOf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;en&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;other&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;english&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nx"&gt;other&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="nx"&gt;english&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="nx"&gt;codes&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;other&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;LanguageHint&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;pages&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;language&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useSyncExternalStore&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="nx"&gt;subscribe&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nx"&gt;getSnapshot&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nx"&gt;getServerSnapshot&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;page&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;language&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="nx"&gt;pages&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;language&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;page&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;a&lt;/span&gt; &lt;span class="na"&gt;href&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;href&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;title&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;a&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Render this component beneath the English document's H1.&lt;/p&gt;

&lt;p&gt;The important boundary is &lt;code&gt;getServerSnapshot&lt;/code&gt;. The browser snapshot then supplies the preferred language candidate.&lt;/p&gt;

&lt;p&gt;The snapshot returns a language string or &lt;code&gt;null&lt;/code&gt;, rather than a newly created object. The destination lookup happens afterward, so language preference and page availability remain separate decisions.&lt;/p&gt;

&lt;p&gt;The subscription in this simplified example is deliberately a no-op.&lt;/p&gt;

&lt;h2&gt;
  
  
  Which language-selection edge cases should you check?
&lt;/h2&gt;

&lt;p&gt;English comes first. If French appears below English in &lt;code&gt;navigator.languages&lt;/code&gt;, this rule shows no French hint. Being present in the list is not enough. The first non-English language must rank above English.&lt;/p&gt;

&lt;p&gt;The preferred translation does not exist. The code selects the first non-English language, then checks for that page. It does not scan through less-preferred languages until it finds a translation. A missing destination means no hint.&lt;/p&gt;

&lt;p&gt;English is absent. It still needs a matching localized page.&lt;/p&gt;

&lt;p&gt;These cases define what the hint does.&lt;/p&gt;

&lt;h2&gt;
  
  
  Did the Thailand answer cite the French page afterward?
&lt;/h2&gt;

&lt;p&gt;The 2026-10-04 weekly check did not. The Thailand question cited no site at all, as did the new UK ETA question.&lt;/p&gt;

&lt;p&gt;Our weekly script asks ChatGPT 18 real questions with web search on. In that run, pixtidy appeared in 1 of 18 answers. The mention was for a Chinese visa photo question, citing &lt;code&gt;pixtidy.com/china-visa-photo&lt;/code&gt; alongside two other tools.&lt;/p&gt;

&lt;p&gt;For the next weekly comparison, the useful reporting questions are specific: how many of the 18 answers mention pixtidy, whether the Thailand answer cites the French page, and whether French-page arrivals carry the ChatGPT tag.&lt;/p&gt;

&lt;p&gt;There was another problem after arrival. All three French visitors uploaded a photo, and all failed "Whole head fits in the frame." That was a separate cropping bug, fixed the next day and covered in another post.&lt;/p&gt;

&lt;p&gt;Related: &lt;a href="https://dev.to/pixtidy/why-a-ready-made-id-photo-failed-our-browser-framing-check-3086"&gt;Why a ready-made ID photo failed our browser framing check&lt;/a&gt; · &lt;a href="https://dev.to/pixtidy/schengen-visa-photos-are-35x45-mm-turning-millimetre-rules-into-browser-checks-1d74"&gt;Schengen visa photos are 35 45 mm: turning millimetre rules into browser checks&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Disclosure: I built pixtidy.&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>react</category>
      <category>seo</category>
      <category>ai</category>
    </item>
    <item>
      <title>Why a ready-made ID photo failed our browser framing check</title>
      <dc:creator>Jason T.</dc:creator>
      <pubDate>Sun, 04 Oct 2026 16:05:00 +0000</pubDate>
      <link>https://dev.to/pixtidy/why-a-ready-made-id-photo-failed-our-browser-framing-check-3086</link>
      <guid>https://dev.to/pixtidy/why-a-ready-made-id-photo-failed-our-browser-framing-check-3086</guid>
      <description>&lt;p&gt;Treat head centring as a preference: clamp the crop to valid bounds, then test against the rotated image. That fixed our framing bug. A −1.1° rotation left roughly 10 px of blank space at the top corners of a 538 px-wide crop.&lt;/p&gt;

&lt;p&gt;You can check your own photo against these rules for free in your browser on &lt;a href="https://pixtidy.com/thailand-evisa-photo?utm_source=devto&amp;amp;utm_campaign=devto-idcrop" rel="noopener noreferrer"&gt;this page&lt;/a&gt;; the photo is never uploaded (disclosure: I built it).&lt;/p&gt;

&lt;h2&gt;
  
  
  Why did ready-made ID photos fail?
&lt;/h2&gt;

&lt;p&gt;When I reviewed analytics on October 3–4, 2026, five people had uploaded a photo during the week. All five saw "likely to be rejected".&lt;/p&gt;

&lt;p&gt;Three uploads, all on the Thailand e-Visa page, failed only "Whole head fits in the frame". Their background statistics, mean 254 and standard deviation 6, suggested ready-made studio ID photos with white backgrounds. That was a clue, not proof of where the photos came from.&lt;/p&gt;

&lt;p&gt;pixtidy crops photos in the browser to an official spec. Our placement code had two problems: it treated vertical centring as mandatory, then treated the canvas boundary as the image boundary.&lt;/p&gt;

&lt;p&gt;A tightly cropped photo exposed both.&lt;/p&gt;

&lt;h2&gt;
  
  
  How do you keep the whole head inside the crop?
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;Separate the required head ratio from your preferred placement.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Thailand e-Visa photos require a head ratio of 70–80%, JPG up to 3 MB, and a height of at least 160 px. There is no published crown-margin or eye-line rule, so our code centred the head vertically.&lt;/p&gt;

&lt;p&gt;At a preferred 75% head ratio, that centred crop could extend above the image. Studio ID photos often leave little space above the head. Rejecting the centred placement without trying another allowed position was the first bug.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Calculate the allowed interval, then clamp your preference.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;For crop height &lt;code&gt;H&lt;/code&gt;, keeping the crown and chin inside requires:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;   y &amp;lt;= crownY
   y &amp;gt;= chinY - H
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;These give you &lt;code&gt;lo&lt;/code&gt; and &lt;code&gt;hi&lt;/code&gt;. Intersect them with any crown-margin or eye-line ranges required by the spec. Then intersect with the image bounds:&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;clampTop&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
     &lt;span class="nx"&gt;preferredY&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;lo&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;hi&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;H&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="kr"&gt;number&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="kc"&gt;undefined&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;lower&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="nx"&gt;lo&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;upper&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;hi&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;H&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
     &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;lower&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;upper&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kc"&gt;undefined&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;max&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;lower&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;min&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;preferredY&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;upper&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
   &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  What does the rotated-image containment test check?
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;Test the complete crop against actual image coverage.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Local measurements showed that the photo had been rotated by −1.1° to make the eyes level. Rotation left blank canvas corners. The first fix could put the crop at &lt;code&gt;y = 0&lt;/code&gt;, inside the canvas rectangle but outside the real image.&lt;/p&gt;

&lt;p&gt;For a 538 px-wide crop, the corner loss was roughly:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;   538 × sin(1.1°) ≈ 10 px
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The head bounds constrain where you may place the crop. The rotated-image boundary decides whether image content covers that entire crop.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why try 41 positions, and can the scan miss a fit?
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;Search within the allowed interval when the clamped candidate fails.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The second fix tries 41 positions across &lt;code&gt;[lo, hi]&lt;/code&gt;, nearest to the preferred position first, and keeps the first that passes the rotated-image test.&lt;/p&gt;

&lt;p&gt;Here, &lt;code&gt;lo&lt;/code&gt; and &lt;code&gt;hi&lt;/code&gt; already include the intersections 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;scanTop&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
     &lt;span class="nx"&gt;lo&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;hi&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;preferredY&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;insideRotatedImage&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;y&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="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;boolean&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
   &lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="kc"&gt;undefined&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
     &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;lo&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;hi&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kc"&gt;undefined&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;positions&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;Array&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;from&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
       &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;length&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;41&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
       &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;_&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;i&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;lo&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;hi&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nx"&gt;lo&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nx"&gt;i&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;40&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
     &lt;span class="p"&gt;);&lt;/span&gt;
     &lt;span class="nx"&gt;positions&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sort&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
       &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;a&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;b&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;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;abs&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;a&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nx"&gt;preferredY&lt;/span&gt;&lt;span class="p"&gt;)&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;abs&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;b&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nx"&gt;preferredY&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="nx"&gt;positions&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;find&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;insideRotatedImage&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
   &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The measured test photo passed after the fix.&lt;/p&gt;

&lt;h2&gt;
  
  
  What changed in the live tool?
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;Compare the same photo before and after, and preserve legitimate failures.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The live-tool test used a tightly cropped 568×757 photo with the head at 72%. Before the fix, framing failed. Afterward, it passed the &lt;a href="https://pixtidy.com/thailand-evisa-photo?utm_source=devto&amp;amp;utm_campaign=devto-idcrop" rel="noopener noreferrer"&gt;Thailand e-Visa framing check&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;It still failed the &lt;a href="https://pixtidy.com/us-visa-photo?utm_source=devto&amp;amp;utm_campaign=devto-idcrop" rel="noopener noreferrer"&gt;US visa framing check&lt;/a&gt;, correctly. The US range is 50–69%, and the head already occupied 72% of the photo.&lt;/p&gt;

&lt;p&gt;Face measurements are not sent to analytics. I investigated with a &lt;code&gt;console.log&lt;/code&gt; behind a window flag in a local development build, then removed it.&lt;/p&gt;

&lt;p&gt;Two new unit tests were added: a tightly cropped photo, and a tilted photo with a custom in-bounds predicate. The existing "face far off-centre stays out of bounds" test still passes. The suite totals 363 tests.&lt;/p&gt;

&lt;h2&gt;
  
  
  Which edge cases should you keep testing?
&lt;/h2&gt;

&lt;p&gt;Tight top margins: move the crop within the allowed interval before rejecting a preferred position.&lt;/p&gt;

&lt;p&gt;Rotated corners: a canvas-bounded crop can still contain blank space. Test actual image coverage.&lt;/p&gt;

&lt;p&gt;Incompatible framing: keep the 72% head failing under the US rule, and preserve the far-off-centre regression.&lt;/p&gt;

&lt;p&gt;A tool's framing pass does not promise official acceptance.&lt;/p&gt;

&lt;p&gt;Related: &lt;a href="https://dev.to/pixtidy/checking-passport-photos-against-government-specs-in-the-browser-with-mediapipe-34be"&gt;Checking passport photos against government specs in the browser with MediaPipe&lt;/a&gt; · &lt;a href="https://dev.to/pixtidy/schengen-visa-photos-are-35x45-mm-turning-millimetre-rules-into-browser-checks-1d74"&gt;Schengen visa photos are 35 45 mm: turning millimetre rules into browser checks&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Disclosure: I built pixtidy.&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>javascript</category>
      <category>typescript</category>
      <category>computervision</category>
    </item>
    <item>
      <title>Schengen visa photos are 35 45 mm: turning millimetre rules into browser checks</title>
      <dc:creator>Jason T.</dc:creator>
      <pubDate>Sat, 03 Oct 2026 16:18:58 +0000</pubDate>
      <link>https://dev.to/pixtidy/schengen-visa-photos-are-35x45-mm-turning-millimetre-rules-into-browser-checks-1d74</link>
      <guid>https://dev.to/pixtidy/schengen-visa-photos-are-35x45-mm-turning-millimetre-rules-into-browser-checks-1d74</guid>
      <description>&lt;p&gt;Schengen requires a 35×45 mm paper photo, with a 32–36 mm chin-to-crown span. At pixtidy's chosen 600 dpi, the export is 827×1063 pixels. The head-height calculation below uses annotated chin and crown coordinates. There is no common Schengen digital-upload specification.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Update, 5 October 2026:&lt;/strong&gt; consulate checklists in the US can differ from 35 × 45 mm. Spain's Consulate General in Chicago asks for photos of approximately 2 × 2 inches, and the Embassy of Switzerland in the USA accepts 35 × 45 mm or a US standard-sized passport photo. Check the checklist of the consulate where you apply before printing.&lt;/p&gt;

&lt;p&gt;You can check your own photo against these rules for free in your browser on &lt;a href="https://pixtidy.com/schengen-visa-photo?utm_source=devto&amp;amp;utm_campaign=devto-schengen" rel="noopener noreferrer"&gt;this page&lt;/a&gt;; the photo is never uploaded (disclosure: I built it).&lt;/p&gt;

&lt;h2&gt;
  
  
  What measurements should you start with?
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Keep the paper size and head size as separate inputs.&lt;/strong&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;I built pixtidy, whose free check uses MediaPipe face detection in your browser. Your photo is never uploaded. The useful engineering question is how to connect physical photo rules to measurements you can actually test.&lt;/p&gt;

&lt;p&gt;Start with the &lt;a href="https://home-affairs.ec.europa.eu/document/download/5bb16566-c8c2-4afb-b038-530f488cb72a_en?filename=icao_photograph_guidelines_en.pdf" rel="noopener noreferrer"&gt;European Commission ICAO photo guidelines&lt;/a&gt; and &lt;a href="https://kuwait.diplo.de/resource/blob/2079290/153d98a9ab09f4702a0d796b5000a6f6/pdf-bio-photo-data.pdf" rel="noopener noreferrer"&gt;German Foreign Office visa photo instructions&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;For the geometry, keep these values explicit:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;photoWidthMm&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;35&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;photoHeightMm&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;45&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;minHeadHeightMm&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;maxHeadHeightMm&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;36&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The first two describe the finished paper photo. The last two describe the chin-to-crown span within it.&lt;/p&gt;

&lt;h2&gt;
  
  
  How do you convert 35×45 mm to pixels at 600 dpi?
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Apply the export resolution to each paper dimension.&lt;/strong&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Use millimetres divided by 25.4, then multiply by the resolution in dots per inch:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&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;600&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;mmToPx&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;mm&lt;/span&gt; &lt;span class="o"&gt;=&amp;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;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="nx"&gt;dpi&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;widthPx&lt;/span&gt; &lt;span class="o"&gt;=&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;photoWidthMm&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;   &lt;span class="c1"&gt;// 827&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;heightPx&lt;/span&gt; &lt;span class="o"&gt;=&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;photoHeightMm&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// 1063&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The calculations are:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Width:  35 × 600 / 25.4 ≈ 827 pixels
Height: 45 × 600 / 25.4 ≈ 1063 pixels
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That is pixtidy's &lt;strong&gt;827×1063-pixel export at 600 dpi&lt;/strong&gt;. The rounding gives you whole-pixel image dimensions.&lt;/p&gt;

&lt;p&gt;I would label this in an interface as "600 dpi export dimensions." There is no common Schengen digital-upload specification.&lt;/p&gt;

&lt;h2&gt;
  
  
  How do you obtain a chin-to-crown measurement?
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Make the measurement endpoints explicit before checking the range.&lt;/strong&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;MediaPipe face detection is the detection step in pixtidy. For this walkthrough, I would use &lt;strong&gt;manual annotation&lt;/strong&gt; for the chin-to-crown measurement: display the photo and ask you to mark the crown and chin. This is a proposed measurement workflow, not a claim that pixtidy provides an annotation feature.&lt;/p&gt;

&lt;p&gt;The annotations would look like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Top of photo
┌─────────────────────────┐
│                         │
│   Crown ───────── yCrown│
│          ↕              │
│      Measured span      │
│          ↕              │
│   Chin ────────── yChin │
│                         │
└─────────────────────────┘
Bottom of photo
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;With vertical coordinates increasing down the image, subtract the crown coordinate from the chin coordinate:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;headHeightPx&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;yChin&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nx"&gt;yCrown&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The important work happens before that subtraction. In this workflow, you identify the endpoints on the photo.&lt;/p&gt;

&lt;p&gt;If annotations are made on a scaled browser preview, convert the span to export coordinates:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;exportHeadHeightPx&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;
  &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;previewChinY&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nx"&gt;previewCrownY&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;
  &lt;span class="nx"&gt;heightPx&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="nx"&gt;previewHeightPx&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This gives you a concrete way to convert the annotated span into export pixels.&lt;/p&gt;

&lt;h2&gt;
  
  
  What does a worked head-height check look like?
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Calculate proportional bounds, then test the annotated span.&lt;/strong&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The head-height rule is &lt;strong&gt;32–36 mm within a 45 mm-high photo&lt;/strong&gt;. As a fraction of image height, that is approximately &lt;strong&gt;71–80%&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Keep the original fractions in code:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;minHeadPx&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;heightPx&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nx"&gt;minHeadHeightMm&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="nx"&gt;photoHeightMm&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;maxHeadPx&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;heightPx&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nx"&gt;maxHeadHeightMm&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="nx"&gt;photoHeightMm&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;// minHeadPx ≈ 755.91&lt;/span&gt;
&lt;span class="c1"&gt;// maxHeadPx = 850.4&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For a worked boundary example, take an annotated span of &lt;strong&gt;850 pixels&lt;/strong&gt;, the whole-pixel value just below the calculated upper bound. This is an arithmetic example, not a measurement from a real applicant's photo.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;measuredHeadPx&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;floor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;maxHeadPx&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// 850&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;withinHeadHeightRange&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;
  &lt;span class="nx"&gt;measuredHeadPx&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="nx"&gt;minHeadPx&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt;
  &lt;span class="nx"&gt;measuredHeadPx&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="nx"&gt;maxHeadPx&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;equivalentHeadMm&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;
  &lt;span class="nx"&gt;measuredHeadPx&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="nx"&gt;heightPx&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nx"&gt;photoHeightMm&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;// withinHeadHeightRange: true&lt;/span&gt;
&lt;span class="c1"&gt;// equivalentHeadMm: approximately 35.98&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The annotated distance passes because 850 is above approximately 755.91 and below 850.4. Translated back into the paper-photo proportions, it represents approximately 35.98 mm.&lt;/p&gt;

&lt;p&gt;The appropriate result label is &lt;strong&gt;"Measured head height is within range."&lt;/strong&gt; It describes exactly what the comparison established.&lt;/p&gt;

&lt;h2&gt;
  
  
  Which measurement edge cases need special handling?
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Handle these two cases before presenting a result.&lt;/strong&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;A resized preview.&lt;/strong&gt; The approximately 756–850-pixel range belongs to the 1063-pixel-high export.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;A rounded boundary.&lt;/strong&gt; "71–80%" and "756–850 pixels" are useful display labels, but they are approximations.&lt;/p&gt;

&lt;p&gt;Both cases can produce a misleading answer even when your annotation endpoints are correct.&lt;/p&gt;

&lt;h2&gt;
  
  
  What must you inspect beyond the pixel checks?
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Review appearance separately from geometry.&lt;/strong&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The background must be &lt;strong&gt;plain, light and even&lt;/strong&gt;, ideally neutral grey, with no shadows. Your face must be centred, sharp and evenly lit.&lt;/p&gt;

&lt;p&gt;Check your expression: neutral, mouth closed, looking straight at the camera.&lt;/p&gt;

&lt;p&gt;If you wear glasses, there must be no glare or tinted lenses, and the frames must not cover your eyes. Head coverings are allowed only for religious reasons.&lt;/p&gt;

&lt;p&gt;Review those requirements separately. Neither detection nor an in-range measurement guarantees acceptance.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where can you try the browser check?
&lt;/h2&gt;

&lt;p&gt;You can use my free &lt;a href="https://pixtidy.com/schengen-visa-photo?utm_source=devto&amp;amp;utm_campaign=devto-schengen" rel="noopener noreferrer"&gt;Schengen photo tool&lt;/a&gt; or the &lt;a href="https://pixtidy.com/fr/verificateur-photo-visa-passeport?utm_source=devto&amp;amp;utm_campaign=devto-schengen" rel="noopener noreferrer"&gt;French checker&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;When implementing the annotation workflow above, make the endpoints visible alongside the numerical result. That lets you inspect the measurement the code actually tested.&lt;/p&gt;

&lt;p&gt;Related: &lt;a href="https://dev.to/pixtidy/checking-passport-photos-against-government-specs-in-the-browser-with-mediapipe-34be"&gt;Checking passport photos against government specs in the browser with MediaPipe&lt;/a&gt; · &lt;a href="https://dev.to/pixtidy/why-a-ds-160-photo-can-be-too-small-and-how-i-test-it-in-the-browser-5bod"&gt;Why a DS-160 photo can be too small, and how I test it in the browser&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Disclosure: I built pixtidy.&lt;/p&gt;

</description>
      <category>javascript</category>
      <category>webdev</category>
      <category>tutorial</category>
      <category>images</category>
    </item>
    <item>
      <title>Why a DS-160 photo can be too small, and how I test it in the browser</title>
      <dc:creator>Jason T.</dc:creator>
      <pubDate>Sat, 03 Oct 2026 15:37:57 +0000</pubDate>
      <link>https://dev.to/pixtidy/why-a-ds-160-photo-can-be-too-small-and-how-i-test-it-in-the-browser-5bod</link>
      <guid>https://dev.to/pixtidy/why-a-ds-160-photo-can-be-too-small-and-how-i-test-it-in-the-browser-5bod</guid>
      <description>&lt;p&gt;At 600×600, the DS-160 compression limit implies a minimum of 54,000 JPEG bytes. Your file must also stay at or below 240 KB. To test both limits in your browser, measure decoded dimensions and encoded bytes, then review composition separately.&lt;/p&gt;

&lt;p&gt;You can test your own photo's size, bytes and head height with the &lt;a href="https://pixtidy.com/us-visa-photo-checker?utm_source=devto&amp;amp;utm_campaign=devto-ds160" rel="noopener noreferrer"&gt;free DS-160 photo checker&lt;/a&gt; in your browser (disclosure: I built it).&lt;/p&gt;

&lt;p&gt;I built pixtidy, which uses MediaPipe face detection in your browser without uploading your photo. The distinction I care about is what each result checks: file measurements cover the digital limits, while landmark-based measurements cover composition. Some rules need your own review.&lt;/p&gt;

&lt;h2&gt;
  
  
  What are the DS-160 photo requirements in numbers?
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Check the file against each limit independently.&lt;/strong&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The &lt;a href="https://travel.state.gov/content/travel/en/us-visas/visa-information-resources/photos/digital-image-requirements.html" rel="noopener noreferrer"&gt;State Department's digital image requirements&lt;/a&gt; specify:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Property&lt;/th&gt;
&lt;th&gt;Requirement&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Shape&lt;/td&gt;
&lt;td&gt;Square&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Dimensions&lt;/td&gt;
&lt;td&gt;600×600 through 1200×1200 pixels&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Format&lt;/td&gt;
&lt;td&gt;Color JPEG&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;File size&lt;/td&gt;
&lt;td&gt;240 KB or less&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Compression ratio&lt;/td&gt;
&lt;td&gt;No higher than 20:1&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The &lt;a href="https://travel.state.gov/content/travel/en/us-visas/visa-information-resources/photos/photo-composition-template.html" rel="noopener noreferrer"&gt;photo composition template&lt;/a&gt; adds two measurements:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Measurement&lt;/th&gt;
&lt;th&gt;Requirement&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Chin to top of hair&lt;/td&gt;
&lt;td&gt;50–69% of image height&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Eyes, measured up from the bottom&lt;/td&gt;
&lt;td&gt;56–69% of image height&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;You need both sets of checks.&lt;/p&gt;

&lt;h2&gt;
  
  
  How do you check file size and compression together?
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Read dimensions from the decoded image and bytes from the JPEG you intend to submit.&lt;/strong&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;This JavaScript illustrates the measurements. It assumes &lt;code&gt;blob&lt;/code&gt; contains the JPEG being tested. For the ceiling check, I use a conservative decimal interpretation of 240 KB: 240,000 bytes.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;image&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;createImageBitmap&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;blob&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="kd"&gt;const&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="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;image&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;encodedBytes&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;blob&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;size&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;rawRgbBytes&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;height&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;3&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;compressionRatio&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;rawRgbBytes&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="nx"&gt;encodedBytes&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;checks&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;square&lt;/span&gt;&lt;span class="p"&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;height&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;dimensions&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="nx"&gt;width&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="mi"&gt;600&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;width&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="mi"&gt;1200&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt;
    &lt;span class="nx"&gt;height&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="mi"&gt;600&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;height&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="mi"&gt;1200&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;fileSize&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;encodedBytes&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="mi"&gt;240&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;1000&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;compression&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;compressionRatio&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="mi"&gt;20&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;table&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="nx"&gt;encodedBytes&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;rawRgbBytes&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;compressionRatio&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;table&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;checks&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="nx"&gt;image&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;close&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The file-size ceiling and compression limit now appear alongside each other.&lt;/p&gt;

&lt;p&gt;At 600×600, the calculation is:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Raw RGB bytes = 600 × 600 × 3 = 1,080,000
Minimum encoded bytes = 1,080,000 ÷ 20 = 54,000
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;At exactly 54,000 bytes, the ratio is 20:1. Any smaller encoded file exceeds that ratio under this calculation.&lt;/p&gt;

&lt;p&gt;If you export another JPEG, rerun the measurements on that output. Use its file size and dimensions to calculate its compression ratio.&lt;/p&gt;

&lt;h2&gt;
  
  
  How do you turn face coordinates into composition percentages?
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Make the coordinate conversion explicit.&lt;/strong&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;There are two separate jobs here: identifying anatomical positions and converting those positions into percentages.&lt;/p&gt;

&lt;p&gt;For the head-height calculation, the endpoints are the chin and the top of the hair. For the eye-line calculation, you need the eyes' vertical position. Each measurement is expressed as a fraction of image height.&lt;/p&gt;

&lt;p&gt;Here is the measurement layer. Its inputs are normalized vertical coordinates measured down from the top, rather than an assumed set of MediaPipe landmark indices:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;measureComposition&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="nx"&gt;chinYNormalized&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;eyeYNormalized&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;topOfHairYNormalized&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;chinY&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;chinYNormalized&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nx"&gt;imageHeight&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;eyeY&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;eyeYNormalized&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nx"&gt;imageHeight&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;topOfHairY&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;
    &lt;span class="nx"&gt;topOfHairYNormalized&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nx"&gt;imageHeight&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;headFraction&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;
    &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;chinY&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nx"&gt;topOfHairY&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="nx"&gt;imageHeight&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;eyesFromBottom&lt;/span&gt; &lt;span class="o"&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;eyeY&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="nx"&gt;imageHeight&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;chinY&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nx"&gt;eyeY&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nx"&gt;topOfHairY&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;headPercent&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;headFraction&lt;/span&gt; &lt;span class="o"&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;eyesFromBottomPercent&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;eyesFromBottom&lt;/span&gt; &lt;span class="o"&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;headInRange&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
      &lt;span class="nx"&gt;headFraction&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="mf"&gt;0.50&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;headFraction&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="mf"&gt;0.69&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;eyesInRange&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
      &lt;span class="nx"&gt;eyesFromBottom&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="mf"&gt;0.56&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;eyesFromBottom&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="mf"&gt;0.69&lt;/span&gt;
  &lt;span class="p"&gt;};&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  What do the composition limits look like at 600×600?
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Convert the official percentages into pixel boundaries you can inspect.&lt;/strong&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;For a 600-pixel-high image, the permitted chin-to-hair distance is:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Minimum head height = 600 × 0.50 = 300 pixels
Maximum head height = 600 × 0.69 = 414 pixels
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The eye-line rule starts at the bottom. Converted to top-origin coordinates, its boundaries are:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;At 56% from bottom: eyeY = 600 × (1 - 0.56) = 264
At 69% from bottom: eyeY = 600 × (1 - 0.69) = 186
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Substituting the boundary coordinate &lt;code&gt;264&lt;/code&gt; gives &lt;code&gt;(600 - 264) / 600 * 100 = 56%&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;For the head measurement, keep the chin coordinate explicit. A top-of-hair coordinate of &lt;code&gt;chinY - 300&lt;/code&gt; gives:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Head percentage
= (chinY - (chinY - 300)) / 600 × 100
= 50%
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That is a boundary calculation. It illustrates the percentage conversion rather than how the coordinates are obtained.&lt;/p&gt;

&lt;h2&gt;
  
  
  Which mistakes still need manual review?
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Check these cases before relying on a numerical result.&lt;/strong&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;Glasses pass unnoticed.&lt;/strong&gt; Glasses are not allowed except for a medical reason with a signed statement. Pixtidy does not check glasses.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Filters and AI edits.&lt;/strong&gt; Photos must not be edited with filters or AI.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where can you test your photo?
&lt;/h2&gt;

&lt;p&gt;You can use the &lt;a href="https://pixtidy.com/us-visa-photo-checker?utm_source=devto&amp;amp;utm_campaign=devto-ds160" rel="noopener noreferrer"&gt;free pixtidy checker&lt;/a&gt; in your browser, without uploading your photo. There is also a &lt;a href="https://pixtidy.com/guides/ds-160-photo-rejected" rel="noopener noreferrer"&gt;full DS-160 photo rejection guide&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Use the measurements to find specific issues. Review the rules outside the checker's scope yourself.&lt;/p&gt;

&lt;p&gt;Related: &lt;a href="https://dev.to/pixtidy/hitting-a-file-size-window-with-canvastoblob-fitting-passport-photos-into-54-240-kb-86e"&gt;Hitting a file-size window with canvas.toBlob: fitting passport photos into 54–240 KB&lt;/a&gt; · &lt;a href="https://dev.to/pixtidy/checking-passport-photos-against-government-specs-in-the-browser-with-mediapipe-34be"&gt;Checking passport photos against government specs in the browser with MediaPipe&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Disclosure: I built pixtidy.&lt;/p&gt;

</description>
      <category>javascript</category>
      <category>webdev</category>
      <category>tutorial</category>
      <category>images</category>
    </item>
    <item>
      <title>US passport photo costs in 2026: Walgreens, USPS, PhotoAiD and a DIY option</title>
      <dc:creator>Jason T.</dc:creator>
      <pubDate>Thu, 01 Oct 2026 16:18:52 +0000</pubDate>
      <link>https://dev.to/pixtidy/us-passport-photo-costs-in-2026-walgreens-usps-photoaid-and-a-diy-option-52of</link>
      <guid>https://dev.to/pixtidy/us-passport-photo-costs-in-2026-walgreens-usps-photoaid-and-a-diy-option-52of</guid>
      <description>&lt;p&gt;I built pixtidy, one of the options below. I'm its solo developer. This comparison includes my product, with its limits alongside the paid alternatives. Buy the format your application needs, rather than choosing by price alone.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the listed price buys
&lt;/h2&gt;

&lt;p&gt;A passport photo price can describe different purchases. A pair of finished prints is different from a digital file. Having someone take the picture is different from taking it yourself and paying for processing afterward.&lt;/p&gt;

&lt;p&gt;Here are the listed prices and what each includes. Prices change and vary by location.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Option&lt;/th&gt;
&lt;th&gt;Price&lt;/th&gt;
&lt;th&gt;What you receive&lt;/th&gt;
&lt;th&gt;What you do yourself&lt;/th&gt;
&lt;th&gt;Source and price check date&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Walgreens, in store&lt;/td&gt;
&lt;td&gt;$16.99&lt;/td&gt;
&lt;td&gt;Two printed photos plus a digital copy by email; someone takes the photo with proper lighting and a backdrop&lt;/td&gt;
&lt;td&gt;Visit the store for the photo&lt;/td&gt;
&lt;td&gt;
&lt;a href="https://photo.walgreens.com/store/passport-photos" rel="noopener noreferrer"&gt;Walgreens&lt;/a&gt;, October 1, 2026&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;USPS&lt;/td&gt;
&lt;td&gt;$15&lt;/td&gt;
&lt;td&gt;Passport photos at participating Post Offices, with someone taking the photo and providing prints&lt;/td&gt;
&lt;td&gt;Visit a participating Post Office&lt;/td&gt;
&lt;td&gt;
&lt;a href="https://about.usps.com/newsroom/local-releases/ny/2025/0602-midtown-post-office-to-host-passport-fair.htm" rel="noopener noreferrer"&gt;USPS notice, published June 2, 2025&lt;/a&gt;, October 1, 2026&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;PhotoAiD&lt;/td&gt;
&lt;td&gt;$13.95, Premium plan&lt;/td&gt;
&lt;td&gt;AI cropping and background adjustment, followed by a person's review&lt;/td&gt;
&lt;td&gt;Take the initial photo&lt;/td&gt;
&lt;td&gt;
&lt;a href="https://photoaid.com/en/us-passport-photo" rel="noopener noreferrer"&gt;PhotoAiD US passport photo page&lt;/a&gt;, October 1, 2026&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;pixtidy&lt;/td&gt;
&lt;td&gt;$9.99 digital; $12.99 with a 4x6 print sheet&lt;/td&gt;
&lt;td&gt;A digital photo, or the digital photo with a sheet you print yourself; checking is free before payment&lt;/td&gt;
&lt;td&gt;Take the photo and, if needed, print the sheet&lt;/td&gt;
&lt;td&gt;
&lt;a href="https://pixtidy.com/us-passport-photo?utm_source=devto&amp;amp;utm_campaign=devto-costs" rel="noopener noreferrer"&gt;pixtidy&lt;/a&gt;, October 1, 2026&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;CVS's site returned 403 when checked on October 1, 2026, so I am not quoting a price.&lt;/p&gt;

&lt;p&gt;The distinction I would watch most closely is "print sheet" versus "printed photos." Those labels describe different deliverables. A downloadable sheet leaves a printing step between purchase and a paper application. Comparing its listed price with an in-store price without that distinction makes the purchase look more equivalent than it is.&lt;/p&gt;

&lt;p&gt;The PhotoAiD price above is specifically the Premium plan listed on its US passport photo page. I would compare that named plan with the service I actually wanted, rather than treating an online provider's price as interchangeable with a store's package.&lt;/p&gt;

&lt;h2&gt;
  
  
  Which option fits your application
&lt;/h2&gt;

&lt;h3&gt;
  
  
  DS-11 or DS-82 by mail
&lt;/h3&gt;

&lt;p&gt;Paper applications need a printed 2x2-inch photo. A digital file alone does not meet that format requirement. Start with the required output before deciding how to get it. See the &lt;a href="https://travel.state.gov/content/travel/en/passports/how-apply/photos.html" rel="noopener noreferrer"&gt;State Department photo guidance&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;For someone who wants the photography and printing handled for them, I would choose Walgreens or a participating Post Office. Their advantage over my self-service option is practical: someone else handles the light and backdrop, and you receive prints. That is a reason to pay for the service, even if a digital option has a lower listed price.&lt;/p&gt;

&lt;p&gt;If you prefer doing the photography yourself, distinguish between buying a file and finishing the application-ready print. The table's print-sheet option belongs in that workflow, rather than in the same category as walking away with finished photos.&lt;/p&gt;

&lt;h3&gt;
  
  
  Online passport renewal
&lt;/h3&gt;

&lt;p&gt;Online passport renewal needs a digital file. Printed photos are therefore not the output to optimize for. See the &lt;a href="https://travel.state.gov/content/travel/en/passports/how-apply/photos.html" rel="noopener noreferrer"&gt;State Department photo guidance&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;For this situation, my recommendation depends on who you want doing the work. A self-service digital purchase fits someone comfortable taking the picture. Walgreens remains a relevant alternative when you want someone to photograph you, because its package includes an emailed digital copy. PhotoAiD makes more sense when a person's review matters to you.&lt;/p&gt;

&lt;p&gt;I would not dismiss the store option merely because the application is online. The purchase includes help creating the image, not only the medium in which you receive it.&lt;/p&gt;

&lt;h3&gt;
  
  
  DS-160
&lt;/h3&gt;

&lt;p&gt;The DS-160 also needs a digital file. That makes the digital deliverable the starting point here, too.&lt;/p&gt;

&lt;p&gt;Use the same division of labor when evaluating options. Decide whether you want to take the picture yourself or have someone photograph you, then consider whether you want human review. Do not treat the price of a printed-photo package as the whole answer to a digital-file task.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where self-service falls short
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://pixtidy.com/us-passport-photo?utm_source=devto&amp;amp;utm_campaign=devto-costs" rel="noopener noreferrer"&gt;pixtidy&lt;/a&gt; runs in the browser, and the photo is never uploaded. You take the picture yourself, without a human reviewer. It does not retouch faces or replace backgrounds. If the wall is not white enough, it tells you to retake the photo. The refund condition is specific: a full refund applies if an official rejects a photo the checker passed.&lt;/p&gt;

&lt;p&gt;If any of those limits is a dealbreaker for you, one of the other options is the better pick.&lt;/p&gt;

&lt;p&gt;There is also an application rule separate from any product's feature list: the &lt;a href="https://travel.state.gov/content/travel/en/passports/how-apply/photos.html" rel="noopener noreferrer"&gt;State Department&lt;/a&gt; says photos must not be altered with software or filters. That guidance is worth reading before choosing an editing workflow. It does not, by itself, establish that a named service's output will be accepted or rejected.&lt;/p&gt;

&lt;p&gt;If you already have a photo, you can check it against the State Department rules for free, with no sign-up, at &lt;a href="https://pixtidy.com/passport-photo-checker?utm_source=devto&amp;amp;utm_campaign=devto-costs" rel="noopener noreferrer"&gt;pixtidy.com/passport-photo-checker&lt;/a&gt;. The comparison above is also kept as a page with sources and the date each price was checked: &lt;a href="https://pixtidy.com/passport-photo-cost?utm_source=devto&amp;amp;utm_campaign=devto-costs" rel="noopener noreferrer"&gt;pixtidy.com/passport-photo-cost&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Disclosure: I built pixtidy, one of the options above. I'm not affiliated with Walgreens, USPS or PhotoAiD.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Related: &lt;a href="https://dev.to/pixtidy/why-a-ds-160-photo-can-be-too-small-and-how-i-test-it-in-the-browser-5bod"&gt;Why a DS-160 photo can be too small, and how I test it in the browser&lt;/a&gt; · &lt;a href="https://dev.to/pixtidy/checking-passport-photos-against-government-specs-in-the-browser-with-mediapipe-34be"&gt;Checking passport photos against government specs in the browser with MediaPipe&lt;/a&gt;&lt;/p&gt;

</description>
      <category>travel</category>
      <category>productivity</category>
      <category>webdev</category>
      <category>privacy</category>
    </item>
    <item>
      <title>App Store and Google Play screenshot sizes in 2026 (and the rules that get uploads rejected)</title>
      <dc:creator>Jason T.</dc:creator>
      <pubDate>Thu, 01 Oct 2026 06:28:38 +0000</pubDate>
      <link>https://dev.to/pixtidy/app-store-and-google-play-screenshot-sizes-in-2026-and-the-rules-that-get-uploads-rejected-175c</link>
      <guid>https://dev.to/pixtidy/app-store-and-google-play-screenshot-sizes-in-2026-and-the-rules-that-get-uploads-rejected-175c</guid>
      <description>&lt;p&gt;Every release I end up re-checking the same thing: which screenshot sizes does App Store Connect want this year, and what will Google Play reject? Here are the official numbers in one place, plus the rules that trip people up. Sources are linked at the bottom.&lt;/p&gt;

&lt;p&gt;To skip manual resizing, my browser-based &lt;a href="https://pixtidy.com/app-store-screenshots?utm_source=devto&amp;amp;utm_campaign=devto-appshots" rel="noopener noreferrer"&gt;App Store &amp;amp; Google Play screenshot generator&lt;/a&gt; adds captions and exports every size without uploading screenshots (disclosure: I built it).&lt;/p&gt;

&lt;h2&gt;
  
  
  App Store: what you actually have to provide
&lt;/h2&gt;

&lt;p&gt;Apple accepts screenshots only at exact pixel sizes. A few pixels off and the upload fails.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Display&lt;/th&gt;
&lt;th&gt;Accepted portrait sizes (px)&lt;/th&gt;
&lt;th&gt;Required?&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;iPhone 6.9"&lt;/td&gt;
&lt;td&gt;1320×2868, 1290×2796, 1260×2736&lt;/td&gt;
&lt;td&gt;Yes, for iPhone apps (or provide 6.5")&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;iPhone 6.5"&lt;/td&gt;
&lt;td&gt;1284×2778, 1242×2688&lt;/td&gt;
&lt;td&gt;Only if you don't provide 6.9"&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;iPhone 6.3"&lt;/td&gt;
&lt;td&gt;1206×2622, 1179×2556&lt;/td&gt;
&lt;td&gt;No (scaled from 6.5")&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;iPhone 6.1"&lt;/td&gt;
&lt;td&gt;1170×2532, 1125×2436, 1080×2340&lt;/td&gt;
&lt;td&gt;No (scaled from 6.5")&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;iPad 13"&lt;/td&gt;
&lt;td&gt;2064×2752, 2048×2732&lt;/td&gt;
&lt;td&gt;Yes, if your app runs on iPad&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;iPad 11"&lt;/td&gt;
&lt;td&gt;1668×2420, 1668×2388, 1640×2360, 1488×2266&lt;/td&gt;
&lt;td&gt;No (scaled from 13")&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Landscape versions of each size are accepted too. In practice, if you ship one iPhone set at 6.9" and, for iPad apps, one set at 13", the smaller displays are scaled from those.&lt;/p&gt;

&lt;p&gt;Other rules:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;1 to 10 screenshots&lt;/strong&gt; per display size&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;JPG or PNG&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No alpha channel or transparency.&lt;/strong&gt; This one bites people who export PNGs from design tools or from &lt;code&gt;&amp;lt;canvas&amp;gt;&lt;/code&gt;: a canvas PNG always has an alpha channel, even when every pixel is opaque. Exporting JPEG avoids it entirely.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Google Play: rules instead of a size list
&lt;/h2&gt;

&lt;p&gt;Play doesn't publish a fixed list. Phone screenshots must be:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;JPEG or 24-bit PNG, no alpha&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Each side between 320 px and 3,840 px&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The long side no more than twice the short side.&lt;/strong&gt; A tall modern phone capture (for example 1080×2400, which is 2.22:1) breaks this rule, so you can't upload raw screenshots from many current Android phones without adding padding or a background.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;To be eligible for Play's large-format recommendation slots, apps need &lt;strong&gt;at least four screenshots at 1080 px or more&lt;/strong&gt;, in &lt;strong&gt;9:16 portrait (1080×1920 or larger)&lt;/strong&gt; or 16:9 landscape.&lt;/p&gt;

&lt;p&gt;The content guidelines also matter if you add marketing captions:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Taglines should take up &lt;strong&gt;no more than 20% of the image&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Avoid performance, ranking, price or promo claims: &lt;strong&gt;"Best", "#1", "Top", "New", "Free", "Discount", "Sale", "Million Downloads"&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;No calls to action like &lt;strong&gt;"Download now"&lt;/strong&gt; or &lt;strong&gt;"Install now"&lt;/strong&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The feature graphic is separate: &lt;strong&gt;1024×500&lt;/strong&gt;, JPEG or 24-bit PNG, no alpha.&lt;/p&gt;

&lt;h2&gt;
  
  
  A layout that satisfies both stores
&lt;/h2&gt;

&lt;p&gt;What has worked for me is a caption band at the top and the real screenshot below it, scaled to fit, never stretched:&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="c1"&gt;// caption band: 18% on Play (under the 20% limit), ~22% on the App Store&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;capH&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;round&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;target&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;height&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;target&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;store&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;play&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="mf"&gt;0.18&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mf"&gt;0.22&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;pad&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;round&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;target&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;width&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mf"&gt;0.08&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;boxW&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;target&lt;/span&gt;&lt;span class="p"&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;pad&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;boxH&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;target&lt;/span&gt;&lt;span class="p"&gt;.&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;capH&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nx"&gt;pad&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;scale&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;boxW&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="nx"&gt;shot&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;boxH&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="nx"&gt;shot&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="c1"&gt;// fit, keep aspect ratio&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;w&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;round&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;shot&lt;/span&gt;&lt;span class="p"&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;scale&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="nx"&gt;h&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;round&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;shot&lt;/span&gt;&lt;span class="p"&gt;.&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;scale&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="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;target&lt;/span&gt;&lt;span class="p"&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;w&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;y&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;capH&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;round&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;boxH&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nx"&gt;h&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Render it on a canvas at the exact target size, then &lt;code&gt;canvas.toBlob(cb, "image/jpeg", 0.92)&lt;/code&gt; so there is no alpha channel to worry about.&lt;/p&gt;

&lt;p&gt;I got tired of doing this by hand, so I built a small browser tool that does exactly this: you add captions to your screenshots and it exports every size above, checks them against these rules (including flagging the Play-banned words in your captions), and nothing gets uploaded: &lt;a href="https://pixtidy.com/app-store-screenshots?utm_source=devto&amp;amp;utm_campaign=devto-appshots" rel="noopener noreferrer"&gt;App Store &amp;amp; Google Play screenshot generator&lt;/a&gt;. Disclosure: I made it. Previews are free; downloading the full set is a one-time payment.&lt;/p&gt;

&lt;h2&gt;
  
  
  Sources
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Apple: &lt;a href="https://developer.apple.com/help/app-store-connect/reference/screenshot-specifications" rel="noopener noreferrer"&gt;App Store Connect Help — Screenshot specifications&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Google: &lt;a href="https://support.google.com/googleplay/android-developer/answer/9866151" rel="noopener noreferrer"&gt;Play Console Help — Add preview assets to showcase your app&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Both checked October 2026. If you've hit a rejection reason that isn't covered here, I'd like to hear it in the comments.&lt;/p&gt;

&lt;p&gt;Related: &lt;a href="https://dev.to/pixtidy/hitting-a-file-size-window-with-canvastoblob-fitting-passport-photos-into-54-240-kb-86e"&gt;Hitting a file-size window with canvas.toBlob: fitting passport photos into 54–240 KB&lt;/a&gt; · &lt;a href="https://dev.to/pixtidy/checking-passport-photos-against-government-specs-in-the-browser-with-mediapipe-34be"&gt;Checking passport photos against government specs in the browser with MediaPipe&lt;/a&gt;&lt;/p&gt;

</description>
      <category>ios</category>
      <category>android</category>
      <category>mobile</category>
      <category>webdev</category>
    </item>
    <item>
      <title>Hitting a file-size window with canvas.toBlob: fitting passport photos into 54–240 KB</title>
      <dc:creator>Jason T.</dc:creator>
      <pubDate>Wed, 30 Sep 2026 19:21:22 +0000</pubDate>
      <link>https://dev.to/pixtidy/hitting-a-file-size-window-with-canvastoblob-fitting-passport-photos-into-54-240-kb-86e</link>
      <guid>https://dev.to/pixtidy/hitting-a-file-size-window-with-canvastoblob-fitting-passport-photos-into-54-240-kb-86e</guid>
      <description>&lt;p&gt;Government photo upload forms are strict about file size, and they are strict in both directions. The US DS-160 visa form wants a JPEG of at most 240 KB with a compression ratio no higher than 20:1. China's online visa form wants 40–120 KB. Upload a 30 KB file and it gets rejected for being "too compressed"; upload 300 KB and it is too big.&lt;/p&gt;

&lt;p&gt;You can check your own photo against these rules for free in your browser on &lt;a href="https://pixtidy.com/photo-requirements?utm_source=devto&amp;amp;utm_campaign=devto-jpegfit" rel="noopener noreferrer"&gt;this page&lt;/a&gt;; the photo is never uploaded (disclosure: I built it).&lt;/p&gt;

&lt;p&gt;Most people fix this by re-saving in an image editor until it works. When you build a tool that does it automatically, it turns into a neat little search problem. Here is how I handle it in the browser, with no server involved.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 1: turn the rules into a byte window
&lt;/h2&gt;

&lt;p&gt;A "compression ratio" limit is really a &lt;em&gt;minimum&lt;/em&gt; file size in disguise. The ratio compares the raw bitmap size to the compressed file size:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;raw bytes = width × height × 3        (24-bit RGB)
ratio     = raw bytes / file bytes
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For a 600×600 DS-160 photo the raw bitmap is 1,080,000 bytes, so a ratio of at most 20:1 means the file must be at least 54,000 bytes. Put that next to the stated maximum and the rules become a window:&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="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;byteBounds&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;spec&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;PhotoSpec&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;d&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;spec&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;digital&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;d&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kc"&gt;undefined&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;raw&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;spec&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;output&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;widthPx&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nx"&gt;spec&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;output&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;heightPx&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;3&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;fromRatio&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;d&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;maxCompressionRatio&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;ceil&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;raw&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="nx"&gt;d&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;maxCompressionRatio&lt;/span&gt;&lt;span class="p"&gt;)&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="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;minBytes&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;max&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;d&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;minBytes&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;fromRatio&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="na"&gt;maxBytes&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;d&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;maxBytes&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;One more gotcha: official pages say "KB" without saying whether they mean 1000 or 1024 bytes. For a minimum I use ×1024, and for a maximum ×1000. The file then passes whichever way the validator reads it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 2: binary-search the JPEG quality
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;canvas.toBlob(cb, "image/jpeg", quality)&lt;/code&gt; gives you exactly one knob. File size grows with quality, roughly monotonically, so binary search works well. I inject the encoder so the logic can be unit-tested without a canvas:&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;Q_MIN&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mf"&gt;0.3&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;Q_MAX&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="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;fitJpeg&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;T&lt;/span&gt; &lt;span class="kd"&gt;extends&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;size&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="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="nx"&gt;encode&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;quality&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="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;T&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;minBytes&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;maxBytes&lt;/span&gt; &lt;span class="p"&gt;}:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nl"&gt;minBytes&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="nl"&gt;maxBytes&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;iterations&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;8&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;best&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;encode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;Q_MAX&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;best&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;size&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="nx"&gt;maxBytes&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="nx"&gt;best&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;size&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="nx"&gt;minBytes&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;ok&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;result&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;best&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="na"&gt;ok&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;reason&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;too-small&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;result&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;best&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;worst&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;encode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;Q_MIN&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;worst&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;size&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;maxBytes&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;ok&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;reason&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;too-large&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;result&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;worst&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;

  &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;lo&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;Q_MIN&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;hi&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;Q_MAX&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;fit&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;worst&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;for &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;i&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nx"&gt;i&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="nx"&gt;iterations&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nx"&gt;i&lt;/span&gt;&lt;span class="o"&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;q&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;lo&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;hi&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;r&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;encode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;q&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;size&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="nx"&gt;maxBytes&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;fit&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;r&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nx"&gt;lo&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;q&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;hi&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;q&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="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;fit&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;size&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="nx"&gt;minBytes&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;ok&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;result&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;fit&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="na"&gt;ok&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;reason&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;too-small&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;result&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;fit&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A few design choices:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Aim for the largest file that fits, not the first one.&lt;/strong&gt; Officers look at these photos. The more quality you keep under the cap, the fewer "blurry photo" rejections you get.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Check the extremes first.&lt;/strong&gt; If quality 1.0 already fits, stop there. If quality 0.3 is still too large, the pixel dimensions are wrong and no quality setting will fix it. Report that instead of looping.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Eight iterations is plenty.&lt;/strong&gt; That narrows quality to about 0.003, well below the point where file size visibly changes.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;"Too small" is a real failure.&lt;/strong&gt; A flat, mostly white photo can compress below the minimum even at quality 1.0. It's better to report that clearly than to pad the file with junk bytes.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;In the browser the encoder is just a promise wrapper:&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;toBlob&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;c&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;HTMLCanvasElement&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;q&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="o"&gt;=&amp;gt;&lt;/span&gt;
  &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;Blob&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;rej&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;toBlob&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;b&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;b&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="nf"&gt;res&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;b&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;rej&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;encode failed&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;))),&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;image/jpeg&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;q&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;fit&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;fitJpeg&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;q&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;toBlob&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;photo&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;q&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="nf"&gt;byteBounds&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;spec&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Step 3: test it without a browser
&lt;/h2&gt;

&lt;p&gt;Because the encoder is injected, the tests use a fake whose size is a function of quality:&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;fake&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;bytesAt&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;q&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="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;q&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="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;size&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;bytesAt&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;q&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="nx"&gt;q&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="nf"&gt;it&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;returns the largest file under the cap&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;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;r&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;fitJpeg&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;fake&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;q&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;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;round&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;400&lt;/span&gt;&lt;span class="nx"&gt;_000&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nx"&gt;q&lt;/span&gt;&lt;span class="p"&gt;)),&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;minBytes&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;54&lt;/span&gt;&lt;span class="nx"&gt;_000&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;maxBytes&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;240&lt;/span&gt;&lt;span class="nx"&gt;_000&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="nf"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ok&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;toBe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nf"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;size&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;toBeLessThanOrEqual&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;240&lt;/span&gt;&lt;span class="nx"&gt;_000&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nf"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;size&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;toBeGreaterThan&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;235&lt;/span&gt;&lt;span class="nx"&gt;_000&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Where the numbers come from
&lt;/h2&gt;

&lt;p&gt;The limits above come from the official pages (travel.state.gov for DS-160, the Chinese visa application service for China). I keep them, with sources, in an open dataset: &lt;a href="https://github.com/tensam/passport-photo-requirements" rel="noopener noreferrer"&gt;passport-photo-requirements on GitHub&lt;/a&gt; (CC BY 4.0, JSON). There is also a &lt;a href="https://pixtidy.com/photo-requirements?utm_source=devto&amp;amp;utm_campaign=devto-jpegfit" rel="noopener noreferrer"&gt;side-by-side table of requirements by country&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;For the US visa (DS-160) photo specifically, where the 240 KB cap applies, there is a free browser-based checker at &lt;a href="https://pixtidy.com/us-visa-photo-checker?utm_source=devto&amp;amp;utm_campaign=devto-jpegfit" rel="noopener noreferrer"&gt;pixtidy.com/us-visa-photo-checker&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;This code runs inside &lt;a href="https://pixtidy.com/?utm_source=devto&amp;amp;utm_campaign=devto-jpegfit" rel="noopener noreferrer"&gt;pixtidy&lt;/a&gt;, a passport and visa photo maker I built. Face detection, cropping and this size fitting all happen in the browser, so the photo never leaves the device. If you've run into other odd government upload rules, I'd like to hear about them in the comments.&lt;/p&gt;

&lt;p&gt;Related: &lt;a href="https://dev.to/pixtidy/why-a-ds-160-photo-can-be-too-small-and-how-i-test-it-in-the-browser-5bod"&gt;Why a DS-160 photo can be too small, and how I test it in the browser&lt;/a&gt; · &lt;a href="https://dev.to/pixtidy/checking-passport-photos-against-government-specs-in-the-browser-with-mediapipe-34be"&gt;Checking passport photos against government specs in the browser with MediaPipe&lt;/a&gt;&lt;/p&gt;

</description>
      <category>javascript</category>
      <category>webdev</category>
      <category>tutorial</category>
      <category>images</category>
    </item>
    <item>
      <title>Checking passport photos against government specs in the browser with MediaPipe</title>
      <dc:creator>Jason T.</dc:creator>
      <pubDate>Wed, 30 Sep 2026 17:11:26 +0000</pubDate>
      <link>https://dev.to/pixtidy/checking-passport-photos-against-government-specs-in-the-browser-with-mediapipe-34be</link>
      <guid>https://dev.to/pixtidy/checking-passport-photos-against-government-specs-in-the-browser-with-mediapipe-34be</guid>
      <description>&lt;p&gt;Government photo rules read like a spec sheet. The DS-160 (US visa) wants a square JPEG between 600×600 and 1200×1200 px, at most 240 KB, with the head (chin to top of hair) taking 50–69% of the image height and the eyes 56–69% up from the bottom. The Chinese visa wants 354×472 px, 40–120 KB, and the crown 10–70 px below the top edge. India's OCI photo must have a light background that is &lt;em&gt;not&lt;/em&gt; white.&lt;/p&gt;

&lt;p&gt;The checks below run free in the browser in &lt;a href="https://pixtidy.com/?utm_source=devto&amp;amp;utm_medium=article&amp;amp;utm_campaign=devto-mediapipe" rel="noopener noreferrer"&gt;pixtidy&lt;/a&gt;; your photo is never uploaded (disclosure: I built it).&lt;/p&gt;

&lt;p&gt;A spec sheet can be turned into code, so I built a checker that runs entirely in the browser: the photo never leaves the device, and there's no server-side inference to pay for. This post walks through the pipeline and the parts that were harder than expected.&lt;/p&gt;

&lt;h2&gt;
  
  
  The stack
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://www.npmjs.com/package/@mediapipe/tasks-vision" rel="noopener noreferrer"&gt;&lt;code&gt;@mediapipe/tasks-vision&lt;/code&gt;&lt;/a&gt; with two models, served from our own origin:

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;FaceLandmarker&lt;/strong&gt; (478 landmarks + blendshapes + a facial transformation matrix)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;ImageSegmenter&lt;/strong&gt; with the selfie segmentation model&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Plain &lt;code&gt;&amp;lt;canvas&amp;gt;&lt;/code&gt; for rotation, cropping, pixel statistics and JPEG encoding&lt;/li&gt;
&lt;li&gt;No uploads and no third-party scripts
&lt;/li&gt;
&lt;/ul&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;fileset&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;FilesetResolver&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;forVisionTasks&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;WASM_PATH&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// self-hosted wasm&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;face&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;FaceLandmarker&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;createFromOptions&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;fileset&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;baseOptions&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;modelAssetPath&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;/models/face_landmarker.task&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;delegate&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="na"&gt;runningMode&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;IMAGE&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;numFaces&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="c1"&gt;// we need to know if there's more than one&lt;/span&gt;
  &lt;span class="na"&gt;outputFaceBlendshapes&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;outputFacialTransformationMatrixes&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;numFaces: 2&lt;/code&gt; is deliberate. If you ask for one face you can't tell a clean portrait apart from a photo with someone standing behind you, and "no other people in the photo" is a rule almost everywhere.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 1: level the eyes before measuring anything
&lt;/h2&gt;

&lt;p&gt;Phone photos are rarely level. Every measurement downstream (head height, eye line, crop box) assumes an upright head, so the first step is to rotate around the midpoint between the irises:&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;roll&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;rollFromEyes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;irisLeft&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;irisRight&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// atan2 of the eye line, in degrees&lt;/span&gt;
&lt;span class="k"&gt;if &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;abs&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;roll&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;source&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;rotate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;source&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;roll&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;cx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;cy&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nx"&gt;res&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;face&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;detect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;source&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// re-run landmarks on the levelled image&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Is rotating allowed? For in-plane tilt, yes: the US State Department's own photo tool rotates. What you must never do is warp or retouch the face. After rotating, the canvas corners are blank, so the crop search is constrained to rectangles that stay inside real pixels.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 2: finding the top of the head (landmarks stop at the forehead)
&lt;/h2&gt;

&lt;p&gt;This was the least obvious part. Every official spec measures head height from the &lt;strong&gt;chin to the crown&lt;/strong&gt; (top of the hair), but the face mesh stops at the hairline. Using the forehead landmark underestimates head height by roughly 20%, and that is enough to fail a 50–69% window.&lt;/p&gt;

&lt;p&gt;The fix is the segmentation mask. Scan down from the top of the image inside the face's column band and take the first row where several columns are "person":&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="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;findCrownY&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;mask&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;w&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;h&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;x0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;x1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;maxY&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;minColumns&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="k"&gt;for &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;y&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&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;lt;=&lt;/span&gt; &lt;span class="nx"&gt;maxY&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="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;hits&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;for &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;let&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;x0&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;lt;=&lt;/span&gt; &lt;span class="nx"&gt;x1&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="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;mask&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;w&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;x&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="nx"&gt;PERSON&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nx"&gt;hits&lt;/span&gt;&lt;span class="o"&gt;++&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;hits&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="nx"&gt;minColumns&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;y&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two details matter:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Only scan the &lt;strong&gt;middle 60% of the face width&lt;/strong&gt;, so shoulders and raised hands at the edges don't count as "head".&lt;/li&gt;
&lt;li&gt;Require a &lt;strong&gt;minimum run of columns&lt;/strong&gt; (about 10% of the face width) so a few stray mask pixels don't pull the crown up.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If the mask finds nothing (a bald head against a white wall can blend in), fall back to &lt;code&gt;forehead − 0.2 × (chin − forehead)&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Model size matters for a page people open on their phone. I started with the 16 MB multiclass segmenter (hair, face skin, body, background) and replaced it with the ~250 KB person/background selfie model, which treats hair as "person". On my test portraits the crown positions agreed within about 1.5% of head height, which is well inside every spec's tolerance.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 3: solve for the crop, don't guess it
&lt;/h2&gt;

&lt;p&gt;With the crown, chin, eye line and face center known, the crop is a small constraint problem. Each spec gives some of:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;head height as a fraction of the image height (e.g. &lt;code&gt;[0.50, 0.69]&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;eye line as a fraction from the bottom (e.g. &lt;code&gt;[0.56, 0.69]&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;crown margin from the top edge (China: 10–70 px of 472)&lt;/li&gt;
&lt;li&gt;output size and aspect ratio&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Pick the scale that puts the head at the middle of its allowed range, place the eyes in the middle of theirs, center horizontally, then check that the rectangle still sits inside the image. If the person is too close to the camera, no rectangle fits, and we tell them to step back instead of silently producing a bad crop.&lt;/p&gt;

&lt;p&gt;Some specs publish no head-size number at all (the UK digital visa photo, New Zealand passport). For those we use a clearly labelled "recommended framing" rather than inventing a rule.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 4: the checks
&lt;/h2&gt;

&lt;p&gt;Everything after the crop is measured on the &lt;strong&gt;output-sized&lt;/strong&gt; image, because that is what the government system will see.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Rule&lt;/th&gt;
&lt;th&gt;Signal&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Eyes open&lt;/td&gt;
&lt;td&gt;blendshapes &lt;code&gt;eyeBlinkLeft/Right&lt;/code&gt; &amp;lt; 0.5&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Mouth closed&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;jawOpen&lt;/code&gt; &amp;lt; 0.25, plus the lip gap relative to face height&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Facing the camera&lt;/td&gt;
&lt;td&gt;yaw from the facial transformation matrix, warn at 5°, fail at 10°&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Background&lt;/td&gt;
&lt;td&gt;mean and standard deviation of luma over &lt;strong&gt;non-person&lt;/strong&gt; pixels&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Lighting&lt;/td&gt;
&lt;td&gt;mean luma of the face box within 70–210&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Sharpness&lt;/td&gt;
&lt;td&gt;variance of the Laplacian on the face box, resized to 256 px wide&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;One surprise: &lt;strong&gt;pitch&lt;/strong&gt; from MediaPipe's transformation matrix reads 10–17° on perfectly frontal official sample portraits. Failing on pitch would reject good photos, so pitch only ever warns.&lt;/p&gt;

&lt;p&gt;Background checks depend on the spec: "white" (US) needs a mean above 200 with low variance, while OCI needs a light background that is &lt;em&gt;not&lt;/em&gt; white. The same pixels can be a pass for one document and a fail for another, which is why every check reads its thresholds from the spec object instead of hard-coding them.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 5: hitting the file-size window
&lt;/h2&gt;

&lt;p&gt;Many portals have a minimum &lt;em&gt;and&lt;/em&gt; maximum file size (China: 40–120 KB, Australia: 70 KB–3.5 MB) and some cap the compression ratio. &lt;code&gt;canvas.toBlob(cb, "image/jpeg", q)&lt;/code&gt; is monotonic enough in &lt;code&gt;q&lt;/code&gt; that a binary search works:&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="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;fitJpeg&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;encode&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;minBytes&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;maxBytes&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="nx"&gt;iterations&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;8&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;best&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;encode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;best&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;size&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="nx"&gt;maxBytes&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;best&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;size&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="nx"&gt;minBytes&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;best&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;tooSmall&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;best&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;worst&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;encode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mf"&gt;0.3&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;worst&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;size&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;maxBytes&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;tooLarge&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;worst&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;lo&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mf"&gt;0.3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;hi&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="nx"&gt;fit&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;worst&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;for &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;i&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nx"&gt;i&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="nx"&gt;iterations&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nx"&gt;i&lt;/span&gt;&lt;span class="o"&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;q&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;lo&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;hi&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;r&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;encode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;q&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;size&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="nx"&gt;maxBytes&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;fit&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;r&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nx"&gt;lo&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;q&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;hi&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;q&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="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;fit&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;size&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="nx"&gt;minBytes&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;fit&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;tooSmall&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;fit&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;encode&lt;/code&gt; is injected so the search is unit-tested without a canvas. Eight iterations are enough to land within a few KB of the limit, and we take the largest file that fits, because a bigger file means less compression and better quality.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I'd tell anyone building something similar
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Store the source for every number.&lt;/strong&gt; Each spec in the code links to, and has an archived copy of, the government page it came from. Requirements disagree across pages (the Vietnam e-visa portal states both a 2 MB and a 10 MB limit) and you need to be able to say why you chose one.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Test on official sample photos.&lt;/strong&gt; Governments publish "acceptable" examples. If your checker fails them, your thresholds are wrong, and that is how the pitch issue showed up.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Never retouch.&lt;/strong&gt; It's tempting to "fix" backgrounds or smooth skin, but US, UK, Australian and New Zealand guidance explicitly rejects edited photos. Crop, level and resize only.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The spec data (21 documents, with sources) is open on GitHub: &lt;a href="https://github.com/tensam/passport-photo-requirements" rel="noopener noreferrer"&gt;tensam/passport-photo-requirements&lt;/a&gt;, and browsable as a table at &lt;a href="https://pixtidy.com/photo-requirements?utm_source=devto&amp;amp;utm_medium=article&amp;amp;utm_campaign=devto-mediapipe" rel="noopener noreferrer"&gt;pixtidy.com/photo-requirements&lt;/a&gt;. The checker itself is at &lt;a href="https://pixtidy.com/?utm_source=devto&amp;amp;utm_medium=article&amp;amp;utm_campaign=devto-mediapipe" rel="noopener noreferrer"&gt;pixtidy.com&lt;/a&gt;. The check is free; I'm the developer, so feedback on edge cases (glasses glare, very dark skin against a white wall, babies) is very welcome.&lt;/p&gt;

&lt;p&gt;Related: &lt;a href="https://dev.to/pixtidy/hitting-a-file-size-window-with-canvastoblob-fitting-passport-photos-into-54-240-kb-86e"&gt;Hitting a file-size window with canvas.toBlob: fitting passport photos into 54–240 KB&lt;/a&gt; · &lt;a href="https://dev.to/pixtidy/schengen-visa-photos-are-35x45-mm-turning-millimetre-rules-into-browser-checks-1d74"&gt;Schengen visa photos are 35×45 mm: turning millimetre rules into browser checks&lt;/a&gt;&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>javascript</category>
      <category>machinelearning</category>
      <category>computervision</category>
    </item>
  </channel>
</rss>
