<?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: Jack-hao615</title>
    <description>The latest articles on DEV Community by Jack-hao615 (@jackhao615).</description>
    <link>https://dev.to/jackhao615</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%2F4124749%2Fcd97bd48-d54e-4a7a-991e-78d3dad300f4.jpg</url>
      <title>DEV Community: Jack-hao615</title>
      <link>https://dev.to/jackhao615</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/jackhao615"/>
    <language>en</language>
    <item>
      <title>Why we charge $2.99 per headshot instead of a $29 subscription</title>
      <dc:creator>Jack-hao615</dc:creator>
      <pubDate>Sun, 15 Nov 2026 15:53:39 +0000</pubDate>
      <link>https://dev.to/jackhao615/why-we-charge-299-per-headshot-instead-of-a-29-subscription-gl0</link>
      <guid>https://dev.to/jackhao615/why-we-charge-299-per-headshot-instead-of-a-29-subscription-gl0</guid>
      <description>&lt;p&gt;I built &lt;a href="https://amiheadshot.com" rel="noopener noreferrer"&gt;AmiHeadshot&lt;/a&gt; because I needed a headshot for my own team page, and every service I found wanted to sell me a subscription or a 40-photo bundle. I needed four photos. One afternoon, done. The pricing mismatch stuck with me long enough that I ended up building the thing I wanted to buy.&lt;/p&gt;

&lt;p&gt;Here's the case for per-image pricing, and why I think the bundle model is quietly broken for most people who just need a LinkedIn photo or a company page avatar.&lt;/p&gt;

&lt;h2&gt;
  
  
  The bundle math doesn't work for single-use buyers
&lt;/h2&gt;

&lt;p&gt;HeadshotPro charges $29 for 40 headshots. Aragon starts at $35 for 20. BetterPic starts at $35 for 20. These are real prices I checked before building anything.&lt;/p&gt;

&lt;p&gt;If you're a job seeker updating your LinkedIn once a year, or a founder who needs four photos for the about page, you're paying $29-35 and using maybe 5-10% of what you bought. The other 35 photos sit in a zip file you'll never open. The per-photo cost you actually paid isn't $0.73 — it's $7.25 per photo you'll actually use.&lt;/p&gt;

&lt;p&gt;I kept doing this math for people I know. A realtor who needed one listing photo. A grad student who needed a thesis defense portrait. A speaker who needed a conference bio photo. Every single one needed fewer than five images, and every service wanted to sell them twenty or forty.&lt;/p&gt;

&lt;p&gt;The subscription model is even worse. $19/month for unlimited headshots sounds reasonable until you realize most people generate headshots once, maybe twice a year. You're paying $228/year for a task that takes ten minutes.&lt;/p&gt;

&lt;h2&gt;
  
  
  What per-image pricing actually looks like
&lt;/h2&gt;

&lt;p&gt;AmiHeadshot charges $2.99 per image. That's the whole pricing model. No credits, no subscription, no minimum order. You pick from 32 studio styles, choose poses, upload one selfie, and pay for the count you want. Five images cost $14.95. One image costs $2.99.&lt;/p&gt;

&lt;p&gt;The generation quality is the same whether you buy one image or twenty. There's no "premium tier" with better models or faster processing. The AI pipeline (fal direct, identity-preserving edit, face-similarity gate at 0.93) runs identically for every order.&lt;/p&gt;

&lt;p&gt;This means the person who needs one photo for a conference bio pays $2.99 and gets the same result as someone who buys ten for a full team page refresh. The pricing is honest about what you're actually getting.&lt;/p&gt;

&lt;h2&gt;
  
  
  The privacy math matters too
&lt;/h2&gt;

&lt;p&gt;Most headshot services keep your uploaded selfie indefinitely, or bury the deletion policy in a privacy policy you'll never read. Some use uploads for model training. You find out after the fact, if you find out at all.&lt;/p&gt;

&lt;p&gt;AmiHeadshot deletes uploads automatically after 30 days. Never used for model training. Finished images stay downloadable for 90 days. These aren't premium features or privacy tier upgrades — they're the default for every order, including the $2.99 single-image one.&lt;/p&gt;

&lt;p&gt;I think this matters more than pricing, honestly. Your face is biometric data. A service that charges you $29 and then trains on your selfie is charging you twice: once with money, once with your face.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where the bundle model actually wins
&lt;/h2&gt;

&lt;p&gt;I want to be fair here. Bundles make sense if you're a content creator who needs fresh headshots monthly, or an agency generating photos for clients regularly. The per-photo cost drops fast at volume, and the convenience of having a credit balance you can dip into is real.&lt;/p&gt;

&lt;p&gt;But that's not most people. Most people need headshots once, maybe twice. The bundle model optimizes for the power user and charges everyone else for capacity they'll never touch.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I'd change if I were starting over
&lt;/h2&gt;

&lt;p&gt;I'd probably add a small bundle option — say, 5 images for $12.99 — for the people who genuinely need a set. Not because the math demands it, but because some buyers feel more comfortable with a "package" even when the per-unit price is the same or worse. That's a psychology problem, not a pricing problem.&lt;/p&gt;

&lt;p&gt;I'd also be more upfront about the generation time. Five images typically finish in under a minute, but the first-time user doesn't know that. The pricing page should say "five photos, about a minute, $14.95" instead of making you do the math yourself.&lt;/p&gt;

&lt;h2&gt;
  
  
  Try it if you need one photo
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://amiheadshot.com" rel="noopener noreferrer"&gt;AmiHeadshot&lt;/a&gt; is live. 32 styles, $2.99 per image, uploads deleted after 30 days. If you need four photos for a team page, that's $11.96 and about a minute of your time. If you need one photo for a conference bio, that's $2.99.&lt;/p&gt;

&lt;p&gt;We're launching on Product Hunt on November 18 if you want to follow along or ask questions there.&lt;/p&gt;

&lt;p&gt;I'm Jacky, the maker. Happy to answer questions in the comments — about the pricing model, the generation pipeline, or why I think the AI headshot market has a honesty problem.&lt;/p&gt;

</description>
      <category>buildinpublic</category>
      <category>saas</category>
      <category>sideprojects</category>
      <category>startup</category>
    </item>
    <item>
      <title>30 PDF Tools, Zero File Uploads — What Running pdf-lib in the Browser Actually Takes</title>
      <dc:creator>Jack-hao615</dc:creator>
      <pubDate>Mon, 14 Sep 2026 16:04:27 +0000</pubDate>
      <link>https://dev.to/jackhao615/30-pdf-tools-zero-file-uploads-what-running-pdf-lib-in-the-browser-actually-takes-7ih</link>
      <guid>https://dev.to/jackhao615/30-pdf-tools-zero-file-uploads-what-running-pdf-lib-in-the-browser-actually-takes-7ih</guid>
      <description>&lt;p&gt;Every online PDF tool asks you the same uncomfortable question: &lt;em&gt;do you trust us with this file?&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Merge a contract. Redact an ID scan. Compress last year's tax records. The honest answer for most tools is "your file goes to a server, gets processed, and you have to believe our retention policy." We built &lt;a href="https://amipdf.com" rel="noopener noreferrer"&gt;AmiPDF&lt;/a&gt; around the opposite default: &lt;strong&gt;if a task can run in the browser, it must run in the browser.&lt;/strong&gt; That turns out to be about 30 out of our 40+ tools.&lt;/p&gt;

&lt;p&gt;This post is the architecture rundown — what client-side PDF processing actually looks like, where it bites, and the honest list of things we still run server-side.&lt;/p&gt;

&lt;h2&gt;
  
  
  The stack, in one paragraph
&lt;/h2&gt;

&lt;p&gt;Two libraries carry almost everything:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://pdf-lib.js.org/" rel="noopener noreferrer"&gt;pdf-lib&lt;/a&gt;&lt;/strong&gt; for every &lt;em&gt;write&lt;/em&gt; operation: merge, split, rotate, page numbers, watermarks, crop, metadata, encryption.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://mozilla.github.io/pdf.js/" rel="noopener noreferrer"&gt;pdf.js&lt;/a&gt;&lt;/strong&gt; for every &lt;em&gt;read/render&lt;/em&gt; operation: page previews, the workspace viewer, text extraction for search-based redaction.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Everything else is glue. The site itself is static hosting behind a CDN — no accounts, no database, no session state. When you merge three PDFs on AmiPDF, your browser opens the files with &lt;code&gt;FileReader&lt;/code&gt;, hands bytes to pdf-lib, and gets a &lt;code&gt;Blob&lt;/code&gt; back. The file never crosses the network.&lt;/p&gt;

&lt;h2&gt;
  
  
  What is genuinely easy
&lt;/h2&gt;

&lt;p&gt;The bread-and-butter operations are embarrassingly simple once you accept one mental model: &lt;strong&gt;a PDF is just bytes you transform locally.&lt;/strong&gt;&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="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;PDFDocument&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="s1"&gt;pdf-lib&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;merged&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;PDFDocument&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create&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="nx"&gt;file&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="nx"&gt;files&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;src&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;PDFDocument&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;load&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;file&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;arrayBuffer&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;pages&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;merged&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;copyPages&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;src&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;src&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getPageIndices&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="nf"&gt;forEach&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;p&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;merged&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;addPage&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;p&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;out&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;merged&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;save&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That is a working merge tool. Rotate, reorder, extract pages, stamp page numbers, attach watermarks — all the same shape: load, transform, save. None of it needs a server, and none of it ever did. The reason most sites upload your file anyway is that servers made their analytics, quotas, and upsells easier — not because the browser couldn't do it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where it bites
&lt;/h2&gt;

&lt;p&gt;Three problems ate most of our debugging time.&lt;/p&gt;

&lt;h3&gt;
  
  
  1. pdf.js text layers lie about spaces
&lt;/h3&gt;

&lt;p&gt;Our redaction tool works like a search box: type a word, we highlight every occurrence, you click to black it out. That means mapping &lt;em&gt;your&lt;/em&gt; query onto the &lt;em&gt;rendered&lt;/em&gt; text layer. Except pdf.js inserts spaces where a PDF only has glyph-position changes — runs that visually touch get a space inserted between them, and runs that overlap can also produce phantom whitespace.&lt;/p&gt;

&lt;p&gt;The fix is unglamorous: normalize whitespace on both sides, and keep an offset map from normalized positions back to original text-layer nodes so a match lands on the right highlight rect. If you're building anything that searches inside rendered PDFs, budget real time for this. It was the single worst bug of the entire project — search "worked" except on documents where it silently matched nothing.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Memory is the real quota
&lt;/h3&gt;

&lt;p&gt;Server-side tools limit uploads by policy. Client-side tools get limited by physics: a 200 MB PDF is 200 MB in an ArrayBuffer, and &lt;code&gt;pdf-lib&lt;/code&gt; will copy it more than once while saving. We tuned compression to stream where possible and to do page-level work (rotate one page, extract a range) without materializing multiple full copies. Practical ceiling on a mid-range phone is lower than on a desktop, and the honest move is to say so in the UI rather than let the tab crash.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Fonts
&lt;/h3&gt;

&lt;p&gt;Embedding fonts for watermarks and page numbers means carrying font bytes you usually don't have. Standard 14 fonts get you surprisingly far; everything beyond that is a subsetting problem.&lt;/p&gt;

&lt;h2&gt;
  
  
  The honest server-side list
&lt;/h2&gt;

&lt;p&gt;Three categories genuinely cannot run in a browser at acceptable quality, and pretending otherwise would be worse than the upload:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Office ↔ PDF conversion.&lt;/strong&gt; LibreOffice in a headless container does this properly. No browser-side library matches it for fidelity.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;OCR.&lt;/strong&gt; Tesseract has WASM builds, but for scan-heavy documents the native server build is dramatically faster and produces a real text-embedded PDF via its PDF renderer.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;High-fidelity PDF → Word/Excel/PPT.&lt;/strong&gt; Layout reconstruction is a research-grade problem; we run a two-engine pipeline with different speed/quality tradeoffs.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;For those lanes the rules are: files are deleted immediately after the job, we keep no accounts (there are none), and we don't log file contents. "Server only when necessary" only means something if the exceptions are named, so we name them.&lt;/p&gt;

&lt;h2&gt;
  
  
  The economics nobody mentions
&lt;/h2&gt;

&lt;p&gt;Client-side processing isn't just a privacy stance — it inverts the cost model. A conversion server bills you per megabyte forever; a static site bills you (almost) nothing per user. That is why AmiPDF has no signup and no daily limits: the marginal cost of the 30 browser tools is close to zero, so charging or nagging would be artificial scarcity.&lt;/p&gt;

&lt;p&gt;The server lanes are metered and rate-limited because they cost real CPU — and that's fine, because they're the minority of tasks.&lt;/p&gt;

&lt;h2&gt;
  
  
  Try it
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://amipdf.com" rel="noopener noreferrer"&gt;amipdf.com&lt;/a&gt; — 40+ tools, 14 languages, no account. If you have a privacy-sensitive workflow, I'd genuinely like to know which tool you'd want verified as fully local next.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;We're launching on Product Hunt soon — if browser-first tools are your thing, keep an eye out.&lt;/em&gt;&lt;br&gt;
&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fmvxx4tmkozcdjutfu5wf.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fmvxx4tmkozcdjutfu5wf.png" alt=" " width="800" height="479"&gt;&lt;/a&gt;&lt;/p&gt;

</description>
      <category>architecture</category>
      <category>frontend</category>
      <category>javascript</category>
      <category>tools</category>
    </item>
  </channel>
</rss>
