<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:content="http://purl.org/rss/1.0/modules/content/">
  <channel>
    <title>Shridha Blog</title>
    <link>https://www.blog.shridha.tech/</link>
    <description>Notes from Shridha, an engineering studio that also makes free apps: the journey, how it is built, and technical articles.</description>
    <language>en</language>
    <lastBuildDate>Thu, 05 Nov 2026 10:25:20 GMT</lastBuildDate>
    <atom:link href="https://www.blog.shridha.tech/feed.xml" rel="self" type="application/rss+xml" />
    <item>
      <title>How to get a photo under 50 KB without ruining it</title>
      <link>https://www.blog.shridha.tech/get-a-photo-under-a-size-limit</link>
      <guid isPermaLink="true">https://www.blog.shridha.tech/get-a-photo-under-a-size-limit</guid>
      <pubDate>Thu, 05 Nov 2026 00:00:00 GMT</pubDate>
      <description>A way to shrink a picture to a file size limit in the browser: search for the best JPEG quality, shrink pixels only when needed, and test it without a real image.</description>
      <category>tech</category>
      <content:encoded><![CDATA[<p>Many Indian exam and job forms reject a photo or signature over 20 KB, 50 KB or 100 KB. Most people end up trying one online tool after another until a file fits. We built <a href="https://www.tools.apps.shridha.tech/reduce-image-size-kb">Reduce Image Size to KB</a> to do the search properly, in the browser. This is how it works.</p>
<h2 id="the-idea">The idea<a class="anchor" href="#the-idea" aria-label="Link to this section">#</a></h2>
<p>For one picture at one size, a JPEG&#39;s file size goes up as quality goes up. That makes it a search problem: find the highest quality whose file still fits under the limit. A binary search does it in a handful of tries.</p>
<pre><code class="language-js">async function bestQuality(encode, scale, maxBytes, low) {
  const top = await encode(scale, 0.92)
  if (top.size &lt;= maxBytes) return { blob: top, scale, quality: 0.92 }
  const floor = await encode(scale, low)
  if (floor.size &gt; maxBytes) return null // even the lowest quality is too big at this size
  let lo = low, hi = 0.92, best = { blob: floor, scale, quality: low }
  for (let i = 0; i &lt; 7; i++) {
    const mid = (lo + hi) / 2
    const blob = await encode(scale, mid)
    if (blob.size &lt;= maxBytes) { best = { blob, scale, quality: mid }; lo = mid }
    else hi = mid
  }
  return best
}
</code></pre>
<p>Seven halvings narrow the quality range enough that the result lands close to the limit. On a large, noisy 2400 by 3200 photo we tried, the results were 19,921 bytes for a 20 KB limit, 48,258 for 50 KB and 99,765 for 100 KB: always under, and within a few percent.</p>
<h2 id="when-quality-is-not-enough">When quality is not enough<a class="anchor" href="#when-quality-is-not-enough" aria-label="Link to this section">#</a></h2>
<p>A 4 MB photo cannot reach 50 KB by lowering quality alone without turning into mush. Pixels are the other lever. So the search has a ladder of sizes, from full size down to 15%, and two rules:</p>
<ol>
<li>First, keep quality at 50% or better and step down the ladder until a size fits.</li>
<li>Only if nothing fits that way, allow quality as low as 10%.</li>
</ol>
<pre><code class="language-js">for (const low of [0.5, 0.1]) {
  for (const scale of SCALES) { // 1, 0.9, 0.8 ... 0.15
    const hit = await bestQuality(encode, scale, maxBytes, low)
    if (hit) return { ...hit, fits: true }
  }
}
</code></pre>
<p>The order matters. A slightly smaller picture at decent quality almost always looks better than a full-size picture crushed into blocks, and forms rarely care about the pixel size, only the file size.</p>
<h2 id="keep-the-search-separate-from-the-browser">Keep the search separate from the browser<a class="anchor" href="#keep-the-search-separate-from-the-browser" aria-label="Link to this section">#</a></h2>
<p>The search never touches a canvas. It takes an <code>encode(scale, quality)</code> function that returns a <code>Blob</code>, so the same code runs in a test with a fake encoder whose size grows with pixels and quality:</p>
<pre><code class="language-js">const fake = (fullBytes) =&gt; async (scale, quality) =&gt;
  new Blob([new Uint8Array(Math.round(fullBytes * scale * scale * (0.05 + quality)))])
</code></pre>
<p>With that we test the behaviour we care about: a file that already fits is left alone at high quality; quality is lowered before pixels are; a huge image shrinks but keeps quality at 50% or better; the result is never over the limit when it says it fits; and when nothing can fit, it says so rather than pretending.</p>
<h2 id="details-that-bit-us">Details that bit us<a class="anchor" href="#details-that-bit-us" aria-label="Link to this section">#</a></h2>
<ul>
<li><strong>Kilobytes.</strong> Some systems count 1 KB as 1000 bytes and some as 1024. A file of 51,200 bytes is &quot;50 KB&quot; to one and 51.2 KB to the other, so a form using the first rule would reject it. We count 1000 bytes per KB, so the file passes either way.</li>
<li><strong>Transparent PNGs.</strong> JPEG has no transparency, so the picture is drawn onto white first, otherwise transparent areas turn black.</li>
<li><strong>Format support.</strong> <code>canvas.toBlob</code> quietly returns PNG if the browser cannot save the format you asked for. We check the returned type and show a clear message instead.</li>
<li><strong>Browsers differ.</strong> Each browser has its own JPEG encoder, so the same quality gives slightly different sizes. That is fine, because the search measures the real size each time rather than predicting it.</li>
<li><strong>Privacy.</strong> The whole thing runs in your browser. The photo is never sent anywhere, and saved copies do not carry camera details like location.</li>
</ul>
<h2 id="what-it-cannot-do">What it cannot do<a class="anchor" href="#what-it-cannot-do" aria-label="Link to this section">#</a></h2>
<p>If a form wants an exact pixel size (say 200 by 230) as well as a file size, set the pixels first with an image resizer and then reduce the size. And a form that wants a minimum size as well as a maximum needs a check on the result: we make the file as large as fits, but a small original cannot be made bigger.</p>
]]></content:encoded>
    </item>
    <item>
      <title>How Shridha Tools works on your files without uploading them</title>
      <link>https://www.blog.shridha.tech/how-tools-keeps-your-files-private</link>
      <guid isPermaLink="true">https://www.blog.shridha.tech/how-tools-keeps-your-files-private</guid>
      <pubDate>Wed, 04 Nov 2026 00:00:00 GMT</pubDate>
      <description>What happens to a PDF or photo when you use Shridha Tools, how to check it yourself in your browser, and the limits that come with working on your own device.</description>
      <category>docs</category>
      <content:encoded><![CDATA[<p>When you merge a PDF, shrink a photo or sign a document on <a href="https://www.tools.apps.shridha.tech/">Shridha Tools</a>, the file is not sent to a server. This post explains how that works and how you can check it, because a claim like &quot;your files stay on your device&quot; is only worth something if you can test it.</p>
<h2 id="what-happens-to-your-file">What happens to your file<a class="anchor" href="#what-happens-to-your-file" aria-label="Link to this section">#</a></h2>
<ol>
<li>You pick a file. Your browser reads it into memory on your device.</li>
<li>The page&#39;s own code works on it there. PDFs are edited with a library called pdf-lib, and pages are drawn with pdf.js, the PDF reader that is built into Firefox. Photos are resized and compressed with the browser&#39;s built-in drawing surface, the canvas.</li>
<li>The result is made in memory too. When you press Download, the browser saves it from there to your device.</li>
</ol>
<p>Nothing in that path needs a server, so we did not build one.</p>
<h2 id="how-to-check-it-yourself">How to check it yourself<a class="anchor" href="#how-to-check-it-yourself" aria-label="Link to this section">#</a></h2>
<p>Open your browser&#39;s developer tools (F12 on most computers) and go to the <strong>Network</strong> tab. Then use a tool on a file.</p>
<ul>
<li>You will see requests for the page, its scripts and its fonts, all from the Tools site itself.</li>
<li>You will not see a request that carries your file or its contents. Nothing is uploaded when you press the button.</li>
</ul>
<p>The code is written so that the only <code>fetch</code> calls in the site read a QR code image the page has just made on your device, to save it as a file.</p>
<h2 id="what-we-do-see">What we do see<a class="anchor" href="#what-we-do-see" aria-label="Link to this section">#</a></h2>
<p>Tools is delivered by Google Firebase Hosting. Like any web host, it handles standard request details such as your IP address, the time, the page you asked for and your browser type, and may keep them in routine logs. We do not use these logs to identify anyone. We add no analytics, no ads and no tracking tools of our own, and the site sets no cookies. Your theme choice and similar small settings, where used, stay in your browser.</p>
<h2 id="the-limits-of-working-on-your-device">The limits of working on your device<a class="anchor" href="#the-limits-of-working-on-your-device" aria-label="Link to this section">#</a></h2>
<p>Doing the work locally is a trade, and some of the costs are yours:</p>
<ul>
<li><strong>Memory.</strong> Big files need a lot of memory. A very large photo, over about 50 megapixels, can fail on an older phone.</li>
<li><strong>Page limits.</strong> &quot;Pages as pictures&quot; in Compress PDF works up to 100 pages at a time. Split large files first.</li>
<li><strong>Compress PDF has two modes.</strong> &quot;Keep text&quot; repacks the file and often saves little. &quot;Pages as pictures&quot; can save a lot, but text can no longer be selected or searched, and links and form fields stop working. The tool says so and shows the new size before you download.</li>
<li><strong>Password-protected PDFs</strong> cannot be opened. Remove the password first.</li>
<li><strong>Photos lose camera details.</strong> Resized and compressed photos are redrawn, so details such as the location are not copied into the new file.</li>
</ul>
<h2 id="why-we-built-it-this-way">Why we built it this way<a class="anchor" href="#why-we-built-it-this-way" aria-label="Link to this section">#</a></h2>
<p>Part of it is privacy: the safest place for a file is the one you already control. Part of it is cost. With no server handling your files, an idle tool costs almost nothing, and that is what lets us keep it free without ads. The next post covers that: <a href="/why-no-ads">why Shridha has no ads</a>.</p>
]]></content:encoded>
    </item>
    <item>
      <title>Daylight saving in a time zone planner: the bugs that hide in a 25-hour day</title>
      <link>https://www.blog.shridha.tech/daylight-saving-in-a-time-zone-planner</link>
      <guid isPermaLink="true">https://www.blog.shridha.tech/daylight-saving-in-a-time-zone-planner</guid>
      <pubDate>Tue, 03 Nov 2026 00:00:00 GMT</pubDate>
      <description>How Timezone Buddy keeps meeting times right when clocks change, with the code for converting local time to a moment, and the bug we fixed where a day was always 24 hours long.</description>
      <category>tech</category>
      <content:encoded><![CDATA[<p>A time zone planner looks simple: take everyone&#39;s local time, line the clocks up, find a time that is not the middle of anyone&#39;s night. The hard part is that clocks change, and not at the same time everywhere. This post covers what we learned building <a href="https://www.zonebuddy.apps.shridha.tech/">Timezone Buddy</a>, with the code that matters.</p>
<h2 id="do-not-write-down-offsets">Do not write down offsets<a class="anchor" href="#do-not-write-down-offsets" aria-label="Link to this section">#</a></h2>
<p>The first rule is to never store &quot;India is UTC+5:30&quot; or &quot;New York is UTC-5&quot;. New York is UTC-5 in winter and UTC-4 in summer, and the switch dates move around by country and by year. A table of offsets goes wrong the day a government changes its rules.</p>
<p>Browsers already carry the full time zone database, and <code>Intl.DateTimeFormat</code> can tell you the wall-clock time in any zone at any moment. From that you can work out the offset for that exact moment:</p>
<pre><code class="language-js">/** Minutes that `zone` is ahead of UTC at the moment `ms`. */
function offsetMinutes(zone, ms) {
  const p = wallClock(zone, ms) // year, month, day, hour, minute, second in that zone
  const asUtc = Date.UTC(p.y, p.m - 1, p.d, p.h, p.min, p.sec)
  return Math.round((asUtc - Math.floor(ms / 1000) * 1000) / 60000)
}
</code></pre>
<p><code>wallClock</code> is a small helper over <code>Intl.DateTimeFormat(...).formatToParts(ms)</code>. The browser does all the daylight saving maths, and the code never mentions a date when clocks change.</p>
<h2 id="from-9-00-on-4-november-in-kolkata-to-a-moment">From &quot;9:00 on 4 November in Kolkata&quot; to a moment<a class="anchor" href="#from-9-00-on-4-november-in-kolkata-to-a-moment" aria-label="Link to this section">#</a></h2>
<p>The reverse direction is trickier. A person types a local time, and you need the exact moment it means. The offset depends on the moment, which is what you are trying to find. The fix is to guess, then correct:</p>
<pre><code class="language-js">function zonedToInstant(zone, y, m, d, h, min) {
  const guess = Date.UTC(y, m - 1, d, h, min)
  const first = guess - offsetMinutes(zone, guess) * 60000
  return guess - offsetMinutes(zone, first) * 60000
}
</code></pre>
<p>The first pass uses the offset at the wrong moment, and the second pass corrects it using the offset at the first answer. Two passes are enough for real zones. We test it by round trip: convert a local time to a moment and back, in zones like Kolkata, Kathmandu (UTC+5:45), New York, London, Sydney and Auckland, and check we get the same time.</p>
<p>Two clock times are special. When clocks go forward, an hour does not exist (2:30 on that morning). When they go back, an hour happens twice. The code above gives a real moment in both cases: for the repeated hour it returns the first of the two, and for the missing time it returns the moment that reads an hour earlier on the clock (so 2:30 on a spring-forward morning in New York becomes 1:30). That is consistent and never crashes, but it is a choice, and a more careful app would warn the person when they pick one of these times.</p>
<h2 id="the-bug-a-day-is-not-always-24-hours">The bug: a day is not always 24 hours<a class="anchor" href="#the-bug-a-day-is-not-always-24-hours" aria-label="Link to this section">#</a></h2>
<p>Our first version of the timeline made 24 hourly slots starting at midnight in the first person&#39;s zone. That is wrong on two days a year in most places:</p>
<ul>
<li>When clocks go forward, the day has 23 hours, so 24 slots spill into the next day.</li>
<li>When clocks go back, the day has 25 hours, so 24 slots miss the last hour.</li>
</ul>
<p>We noticed because 1 November 2026 was a day when US clocks went back. The fix is to build the day from one local midnight to the next, and count the slots in between:</p>
<pre><code class="language-js">const start = zonedToInstant(zone, y, m, d, 0, 0)
const end = zonedToInstant(zone, nextY, nextM, nextD, 0, 0)
const count = Math.round((end - start) / SLOT) // SLOT is 30 minutes
</code></pre>
<p>With half-hour slots this gives 48 on a normal day, 46 on a 23-hour day and 50 on a 25-hour day. Lord Howe Island in Australia changes clocks by only 30 minutes, so its day has 47 slots. Our tests check all of these, and also that each day starts and ends at local midnight.</p>
<h2 id="two-countries-two-change-dates">Two countries, two change dates<a class="anchor" href="#two-countries-two-change-dates" aria-label="Link to this section">#</a></h2>
<p>The other trap is that neighbours do not switch on the same day. In 2026, the UK moved its clocks back on 25 October and the US on 1 November. For that week, London and New York are 4 hours apart instead of the usual 5. The same happens in March, when the US changes before the UK.</p>
<p>So a team in London and New York has the same shared working window most of the year, but a different one for a week:</p>
<table>
<thead>
<tr>
<th>Dates</th>
<th>London and New York apart</th>
<th>Shared window (both 9:00 to 18:00), London time</th>
</tr>
</thead>
<tbody><tr>
<td>Before 25 Oct</td>
<td>5 hours</td>
<td>14:00 to 18:00</td>
</tr>
<tr>
<td>25 Oct to 1 Nov</td>
<td>4 hours</td>
<td>13:00 to 18:00</td>
</tr>
<tr>
<td>After 1 Nov</td>
<td>5 hours</td>
<td>14:00 to 18:00</td>
</tr>
</tbody></table>
<p>A planner that checks only &quot;today&quot; will give the wrong answer for a meeting booked on the other side of a change. That is why Timezone Buddy works on a chosen date, and warns when anyone&#39;s clocks change within a few days of it.</p>
<h2 id="share-a-moment-not-a-clock-time">Share a moment, not a clock time<a class="anchor" href="#share-a-moment-not-a-clock-time" aria-label="Link to this section">#</a></h2>
<p>When you send someone the meeting time, send the moment and let their device show it locally. In the calendar invite we write the start as a UTC time (<code>DTSTART:20261104T093000Z</code>), so Google Calendar, Apple Calendar and Outlook each show it correctly for the person who opens it, including after clocks change.</p>
<h2 id="what-we-did-not-solve">What we did not solve<a class="anchor" href="#what-we-did-not-solve" aria-label="Link to this section">#</a></h2>
<p>A few places change their clocks at midnight, so the first midnight of the day can be skipped. We pick the first valid time, but we have not tested those zones in a browser. And the planner trusts the browser&#39;s time zone database, so a device with an out-of-date system will be wrong about a recent rule change.</p>
]]></content:encoded>
    </item>
    <item>
      <title>Why Shridha&#39;s apps have no ads</title>
      <link>https://www.blog.shridha.tech/why-no-ads</link>
      <guid isPermaLink="true">https://www.blog.shridha.tech/why-no-ads</guid>
      <pubDate>Mon, 02 Nov 2026 00:00:00 GMT</pubDate>
      <description>Ads need tracking, and tracking costs you privacy and speed. How we keep the apps free without them, and what that rules out.</description>
      <category>journey</category>
      <content:encoded><![CDATA[<p>Free software has to be paid for somehow. The usual answer is ads. We chose not to use them, and this post explains why and what that costs us.</p>
<h2 id="what-ads-would-need">What ads would need<a class="anchor" href="#what-ads-would-need" aria-label="Link to this section">#</a></h2>
<p>An ad on a page is not just a picture. It usually brings extra scripts from other companies, measurement of what you click, and a profile of who you are so the ad can be matched to you. For a tool where you paste a salary, open an invoice or upload a scan, that is the wrong trade. The more private the job, the worse it fits.</p>
<p>Ads also slow pages down and push the useful part around. We would rather the page do its one job and get out of the way.</p>
<h2 id="what-keeps-the-apps-free-instead">What keeps the apps free instead<a class="anchor" href="#what-keeps-the-apps-free-instead" aria-label="Link to this section">#</a></h2>
<p>The answer is mostly <strong>cost</strong>. If an app is cheap enough to run, it does not need to earn money.</p>
<ul>
<li>Most of our apps are static sites. They are files on a host, and many of them do their work in your browser. There is no server running for each visitor.</li>
<li>Where an app does need a server, for example to hold a one-time secret or a short link, we use pay-per-use services. An app that nobody is using costs close to nothing.</li>
<li>We do not keep a fixed monthly bill for each app. That rule decides what we build: if an idea needs something always running, we look for a different way to do it or leave it out.</li>
</ul>
<p>Shridha is also an engineering studio, and these apps are built to need very little to keep going. That is a design choice, not an accident.</p>
<h2 id="what-it-rules-out">What it rules out<a class="anchor" href="#what-it-rules-out" aria-label="Link to this section">#</a></h2>
<p>Saying no to ads and fixed costs means saying no to some ideas. Anything that needs heavy servers all day, such as processing video for you, is hard to offer free. We would rather say so than quietly add a limit later. Bosswala, which does run on servers for staff attendance and shared leads, has a small fee for that reason.</p>
<h2 id="what-it-means-for-you">What it means for you<a class="anchor" href="#what-it-means-for-you" aria-label="Link to this section">#</a></h2>
<ul>
<li>No ads and no tracking tools in the free apps.</li>
<li>No need to make an account to do a simple job.</li>
<li>Where an app offers a sign-in, it is optional.</li>
</ul>
<p>You can see the full list in the <a href="https://www.apps.shridha.tech/">apps catalog</a>. For how the file tools keep your data on your own device, read <a href="/how-tools-keeps-your-files-private">how Tools works on your files without uploading them</a>.</p>
]]></content:encoded>
    </item>
    <item>
      <title>Why Shridha exists</title>
      <link>https://www.blog.shridha.tech/why-shridha-exists</link>
      <guid isPermaLink="true">https://www.blog.shridha.tech/why-shridha-exists</guid>
      <pubDate>Thu, 29 Oct 2026 00:00:00 GMT</pubDate>
      <description>Shridha is a developer studio that also makes free, private, ad-free apps. Why we make them, who they are for, and the rules we build by.</description>
      <category>journey</category>
      <content:encoded><![CDATA[<p>Shridha is an engineering studio. We build web, mobile and cloud products for clients, using AI-assisted engineering, automated tests and CI/CD, and you can see some of that on our <a href="https://www.shridha.tech/work">work page</a>. Alongside that, we make our own apps, free for anyone to use. This post is about those apps and why we make them. Both come from the same place: we like building software that is carefully made and that people can rely on.</p>
<p>Most people who use software for ordinary jobs have met the same three problems. A free plan gets smaller every year. Ads and trackers creep in. Or a tool that should take ten seconds asks you to make an account first.</p>
<p>Our free apps are an answer to that. They are small, useful, free to use, private by default and have no ads. They are for developers, builders and small business owners, and for small businesses in India first, because that is where we saw the biggest gap between what people need and what is free.</p>
<h2 id="what-we-have-built-so-far">What we have built so far<a class="anchor" href="#what-we-have-built-so-far" aria-label="Link to this section">#</a></h2>
<p>The full list lives in the <a href="https://www.apps.shridha.tech/">Shridha apps catalog</a>. A few examples:</p>
<ul>
<li><strong>Tools</strong>: loan, tax and GST calculators, image and PDF tools, QR codes and developer utilities, all running in your browser.</li>
<li><strong>Invoice Maker</strong>: GST invoices, quotes and bills of supply as PDFs, with your details kept on your own device.</li>
<li><strong>Panchang</strong>: tithi, nakshatra and festivals for Indian cities, in Hindi and English.</li>
<li><strong>typeflow</strong>: a typing test that shows which keys slowed you down.</li>
<li><strong>Gupt</strong>: send a password or secret that opens only once.</li>
<li><strong>Smaran Ghanti</strong>: a mala counter and reminders for daily naam jap.</li>
</ul>
<h2 id="the-rules-we-build-by">The rules we build by<a class="anchor" href="#the-rules-we-build-by" aria-label="Link to this section">#</a></h2>
<p><strong>Free stays free.</strong> If an app is free today, we do not move its core features behind a paywall later. We would rather build fewer things than break that.</p>
<p><strong>Private by default.</strong> Where we can, the work happens in your browser, so your files and numbers never reach us. No ads, and no tracking tools.</p>
<p><strong>Accounts are optional.</strong> An account is offered only when it gives you something real, such as saving your results, and the app works without one.</p>
<p><strong>Cheap to run, so the promise is affordable.</strong> We use static hosting and pay-per-use services, so an app that nobody is using costs close to nothing. That is what makes the first rule possible. We wrote more about this in <a href="/why-no-ads">why Shridha has no ads</a>.</p>
<p><strong>One exception, stated plainly.</strong> Bosswala, our attendance and leads app for Indian shops, charges a small fee to cover what it costs to run. It is listed in the catalog with that difference clear.</p>
<h2 id="what-this-blog-is-for">What this blog is for<a class="anchor" href="#what-this-blog-is-for" aria-label="Link to this section">#</a></h2>
<p>It covers the studio and the apps. Two kinds of post. The <strong>journey</strong>: what we decided, what we dropped, and why. And <strong>docs</strong>: how things are built and how they work, written for people who want to check our claims rather than trust them. The next post, <a href="/how-tools-keeps-your-files-private">how Tools works on your files without uploading them</a>, is the first of those.</p>
<p>If there is an everyday job you wish had a free, private tool, <a href="https://www.shridha.tech/contact">tell us</a>.</p>
]]></content:encoded>
    </item>
  </channel>
</rss>
