By Arvind Kumar, Editor, Sarkari Job Selection

Every year, millions of Indian candidates fill in government exam forms. Most of these forms reject your photo unless it meets a narrow spec, for example 200×230 px and 20–50 KB for IBPS bank exams. The usual fix is an "online resizer" that uploads someone's face and signature to a random server. We wanted a version that never uploads anything, so we built one with <canvas> and about 150 lines of plain JavaScript. We also built a small age-eligibility calculator, because "Am I over the age limit?" turns out to be a surprisingly sharp date-math problem.

Here is what was tricky about each one.

Part 1: The photo resizer

Specs differ by exam, so keep them as data

const PRESETS = [ { id: "ibps-photo", w: 200, h: 230, unit: "px", min: 20, max: 50 }, { id: "ibps-sign", w: 140, h: 60, unit: "px", min: 10, max: 20 }, { id: "ssc-sign", w: 6, h: 2, unit: "cm", min: 10, max: 20 }, { id: "upsc-sign", w: 500, h: 500, unit: "px", min: 20, max: 100 }, ]; 

Some notices give sizes in cm, not pixels. We convert using a DPI the user can change (200 by default):

const toPx = (v, unit, dpi) => unit === "cm" ? Math.round(v / 2.54 * dpi) : Math.round(v); 

Only add a preset once you've checked it in the official notification. Where a notice gives only a KB range and no pixel size, leave the pixel fields blank and let the user enter them. Don't guess.

canvas.toBlob has no "target size" option, so binary-search the quality

JPEG size goes down as quality goes down (not perfectly, but closely enough). So we search for the highest quality that still fits under the max:

// encode(W, H, q) -> Promise<ArrayBuffer> async function bestUnderMax(encode, W, H, maxBytes) { let lo = 0.05, hi = 0.95; const top = await encode(W, H, hi); if (top.byteLength <= maxBytes) return { buf: top, q: hi }; const floor = await encode(W, H, lo); if (floor.byteLength > maxBytes) return null; // too many pixels for this budget let best = { buf: floor, q: lo }; for (let i = 0; i < 8; i++) { // 8 steps is plenty const mid = (lo + hi) / 2; const b = await encode(W, H, mid); if (b.byteLength <= maxBytes) { best = { buf: b, q: mid }; lo = mid; } else hi = mid; } return best; } 

The encoder draws onto a white background, so transparent PNGs don't turn black when saved as JPEG:

function encode(img, W, H, q) { const c = document.createElement("canvas"); c.width = W; c.height = H; const x = c.getContext("2d"); x.fillStyle = "#fff"; x.fillRect(0, 0, W, H); x.imageSmoothingQuality = "high"; x.drawImage(img, 0, 0, W, H); // our real code also handles cover/contain return new Promise(r => c.toBlob(b => b.arrayBuffer().then(r), "image/jpeg", q)); } 

If the user didn't fix the pixel size and quality falls below about 50%, we shrink the image by 15% and try again. A slightly smaller, sharp photo looks better than a blocky one.

The weird case: the file is too small

Some forms need a minimum size too (for example "10–20 KB"), and a 140×60 signature can come out at 4 KB. Lowering compression only helps so much. Our fix is to insert a JPEG COM (comment) segment (0xFFFE) right after the SOI marker. Decoders ignore it, and it adds harmless padding bytes:

function padJpeg(buf, minBytes) { const u = new Uint8Array(buf); let need = minBytes - u.length; if (need <= 0) return u; const segs = []; while (need > 0) { const len = Math.min(65533, Math.max(need, 4)); // segment length field max const s = new Uint8Array(len + 2); s[0] = 0xFF; s[1] = 0xFE; s[2] = (len >> 8) & 255; s[3] = len & 255; s.fill(32, 4); segs.push(s); need -= s.length; } const out = new Uint8Array(u.length + segs.reduce((n, s) => n + s.length, 0)); out.set(u.subarray(0, 2), 0); // SOI let off = 2; for (const s of segs) { out.set(s, off); off += s.length; } out.set(u.subarray(2), off); return out; } 

We tell the user when we've padded a file, so nothing happens silently.

Privacy for free

FileReader → Image → canvas → Blob → URL.createObjectURL. The photo never leaves the device, there's no backend to secure, and the tool works on cheap phones. Remember to call URL.revokeObjectURL on the old URL each time you create a new one.

Part 2: The age-limit calculator

A notice says something like "18–27 years as on 01-08-2026". The SSC CHSL 2026 notice spells it out: the candidate must be born not before 02-08-1999 and not later than 01-08-2008. That means:

  • you must have reached the minimum age on the cut-off date, and
  • you must not have reached the maximum age (so "27" really means "below 27").
const eligible = (age, min, max) => age.y >= min && age.y < max; 

Don't use Date in local time

Timezones and DST will quietly shift dates by one day. Parse YYYY-MM-DD yourself, do the math in UTC, and reject impossible dates like 31-02:

function parse(s) { const m = /^(\d{4})-(\d{2})-(\d{2})$/.exec(s || ""); if (!m) return null; const [y, mo, d] = [+m[1], +m[2], +m[3]]; const dt = new Date(Date.UTC(y, mo - 1, d)); return dt.getUTCMonth() === mo - 1 ? { y, m: mo, d } : null; } const daysIn = (y, m) => new Date(Date.UTC(y, m, 0)).getUTCDate(); function age(b, c) { let months = (c.y - b.y) * 12 + (c.m - b.m); if (c.d < b.d) months--; const ay = b.y + Math.floor((b.m - 1 + months) / 12); const am = (b.m - 1 + months) % 12 + 1; const ad = Math.min(b.d, daysIn(ay, am)); // 29 Feb / 31st clamp const days = Math.round( (Date.UTC(c.y, c.m - 1, c.d) - Date.UTC(ay, am - 1, ad)) / 864e5); return { y: Math.floor(months / 12), m: months % 12, d: days }; } 

Test the boundaries the notice gives you

The notice's own dates make good test cases:

DOB Cut-off Age 18–27?
02-08-1999 01-08-2026 26y 11m 30d ✅
01-08-1999 01-08-2026 27y 0m 0d ❌ (reached 27)
01-08-2008 01-08-2026 18y 0m 0d ✅
02-08-2008 01-08-2026 17y 11m 30d ❌
29-02-2000 01-03-2026 26y 0m 1d ✅

Category relaxation is just max + years (for SSC CGL/CHSL 2026: OBC +3, SC/ST +5). Rules vary by exam, though, so we link every quick preset to its official PDF and tell users to confirm there.

Takeaways

  1. Size targets → binary search on quality, then shrink the pixels if quality gets too low.
  2. Minimum-size targets → pad with a JPEG COM segment, and tell the user you did.
  3. Date eligibility → integer y/m/d math in UTC, and use the notice's own boundary dates as unit tests.
  4. Keep specs as data with a source link. Leave out anything you can't verify.

Both tools are free to use (no signup, nothing uploaded): the exam photo & signature resizer and the govt job age calculator. If you know a quality-vs-size heuristic that beats plain binary search, I'd love to hear it in the comments.