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
- Size targets → binary search on quality, then shrink the pixels if quality gets too low.
- Minimum-size targets → pad with a JPEG COM segment, and tell the user you did.
- Date eligibility → integer y/m/d math in UTC, and use the notice's own boundary dates as unit tests.
- 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.
Top comments (0)