<?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: Pianyubook</title>
    <description>The latest articles on DEV Community by Pianyubook (@pianyubook).</description>
    <link>https://dev.to/pianyubook</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%2F4240230%2Ff70f6182-dd13-42f9-ac78-e31c90cd7ef6.png</url>
      <title>DEV Community: Pianyubook</title>
      <link>https://dev.to/pianyubook</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/pianyubook"/>
    <language>en</language>
    <item>
      <title>I had to build a layout engine to turn 4,000 phone photos into a printed book</title>
      <dc:creator>Pianyubook</dc:creator>
      <pubDate>Thu, 12 Nov 2026 05:18:58 +0000</pubDate>
      <link>https://dev.to/pianyubook/i-had-to-build-a-layout-engine-to-turn-4000-phone-photos-into-a-printed-book-16pa</link>
      <guid>https://dev.to/pianyubook/i-had-to-build-a-layout-engine-to-turn-4000-phone-photos-into-a-printed-book-16pa</guid>
      <description>&lt;p&gt;A phone gallery has no rules. A printed book has nothing but rules. That gap is the whole product.&lt;/p&gt;

&lt;p&gt;I built a small service that takes someone's photos and their written (or dictated) stories and produces a print-ready PDF. It's called Pianyu — &lt;a href="https://pianyubook.com" rel="noopener noreferrer"&gt;https://pianyubook.com&lt;/a&gt; — and the part I underestimated was not the UI or the payments. It was page geometry.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why a template library did not work
&lt;/h2&gt;

&lt;p&gt;My first version was a grid of nice templates: one big photo, two photos, photo plus caption. It looked fine on screen and was wrong for print in ways I only saw on paper:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Page breaks landed in the middle of a sentence.&lt;/li&gt;
&lt;li&gt;A landscape photo in a portrait slot was cropped to a portrait crop, losing the face.&lt;/li&gt;
&lt;li&gt;Widow lines: one word alone on a page, because the caption below pushed the text.&lt;/li&gt;
&lt;li&gt;Running heads collided with the top margin.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Templates assume content fits. Books require the content to &lt;em&gt;be measured&lt;/em&gt; and then re-flowed. So the second version is an engine, not a picker.&lt;/p&gt;

&lt;h2&gt;
  
  
  Fixed print geometry first, everything else after
&lt;/h2&gt;

&lt;p&gt;Before any design, I fixed the canvas:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;US Letter, 612 × 792 pt as the page box.&lt;/li&gt;
&lt;li&gt;88 pt side margins, 96 pt bottom margin (the optical bottom looks bigger than it measures).&lt;/li&gt;
&lt;li&gt;Body text 12.5 pt with a 1.6–1.75 line-height; headings 20–21 pt.&lt;/li&gt;
&lt;li&gt;Photos are placed by aspect ratio, never by crop-to-fit. If the slot is portrait and the photo is landscape, the slot changes, not the photo.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Once the geometry is a constant, every layout decision becomes a measurement: does this paragraph fit above the photo, and if not, which page does it continue on. That is the entire engine.&lt;/p&gt;

&lt;h2&gt;
  
  
  The CJK font problem
&lt;/h2&gt;

&lt;p&gt;The product ships in English and Chinese. A full GBK serif is around 18 MB. A subset of the glyphs actually used is about 5.6 MB. On a cold first page load that is the difference between a page that appears and a page that looks broken.&lt;/p&gt;

&lt;p&gt;More importantly: embedding CJK in a PDF reliably was the one thing I got burned on. I started with pdf-lib, which is excellent for documents you will read on a screen, and gave up on it for CJK print output — the results were inconsistent enough that I did not trust a printer to accept them. I moved the final typesetting step to PyMuPDF, which embeds the font file directly and produces what the printer expects.&lt;/p&gt;

&lt;p&gt;If your output is going to physical print and contains CJK, test with the actual printer before you build a product on top of it.&lt;/p&gt;

&lt;h2&gt;
  
  
  The free five pages
&lt;/h2&gt;

&lt;p&gt;The commercial mechanism and the technical one are the same thing: the layout engine can render the first five pages for free, before payment. Nothing is hidden — those five pages are the same code path that produces the paid file, just truncated.&lt;/p&gt;

&lt;p&gt;This changed the funnel completely. "Trust me, it will look good" does not sell a book. Seeing your own grandmother's photo set in a serif, with the caption you wrote underneath, does.&lt;/p&gt;

&lt;h2&gt;
  
  
  Payments were the easy part
&lt;/h2&gt;

&lt;p&gt;I use a merchant of record. Tax, cards, disputes are handled upstream of me, and the webhook is a signed POST that marks an order paid after checking the amount against the SKU. Idempotent, boring, done in an afternoon.&lt;/p&gt;

&lt;p&gt;The unsolved part is that nobody arrives. A five-day-old domain does not get indexed on demand, and a dev.to post does not bring buyers. That is a marketing problem, and it is more stubborn than any of the print bugs above.&lt;/p&gt;

&lt;p&gt;If you want to see what the output actually looks like, two sample books (five real pages each) are here: &lt;a href="https://pianyubook.com" rel="noopener noreferrer"&gt;https://pianyubook.com&lt;/a&gt; — and I'd genuinely like feedback from anyone who has shipped a print PDF a printer did not complain about.&lt;/p&gt;

</description>
      <category>print</category>
      <category>sideprojects</category>
      <category>pdf</category>
      <category>showdev</category>
    </item>
    <item>
      <title>I built a photo-book generator that turns your phone's photo pile into a printed book</title>
      <dc:creator>Pianyubook</dc:creator>
      <pubDate>Thu, 12 Nov 2026 04:56:03 +0000</pubDate>
      <link>https://dev.to/pianyubook/i-built-a-photo-book-generator-that-turns-your-phones-photo-pile-into-a-printed-book-mia</link>
      <guid>https://dev.to/pianyubook/i-built-a-photo-book-generator-that-turns-your-phones-photo-pile-into-a-printed-book-mia</guid>
      <description>&lt;p&gt;Every family has a folder like this: 4,000 vacation photos on one phone, a few hundred screenshots, and a voice memo of my grandmother describing the old house she grew up in.&lt;/p&gt;

&lt;p&gt;Nobody ever opens that folder again. It just sits there, and one day the phone dies.&lt;/p&gt;

&lt;p&gt;I wanted one specific thing out of it: a printed book you can put on a shelf, with the photos on the left and the story on the right.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I built
&lt;/h2&gt;

&lt;p&gt;It's called &lt;strong&gt;Pianyu&lt;/strong&gt; (片语成书, "a few words make a book") — &lt;a href="https://pianyubook.com" rel="noopener noreferrer"&gt;https://pianyubook.com&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The flow is deliberately boring, on purpose:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;You drop in photos and write (or dictate) the story around them.&lt;/li&gt;
&lt;li&gt;The app lays out the pages, typesets the text, and produces a print-ready PDF.&lt;/li&gt;
&lt;li&gt;You see the &lt;strong&gt;first 5 pages rendered for free&lt;/strong&gt; before paying anything.&lt;/li&gt;
&lt;li&gt;If the pages look right, you pay and get the full PDF — $19 for a photo book, $39 for an illustrated book, $79 for a full life story.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The free preview is the whole funnel. People don't trust a layout engine they can't see, and "trust me, it'll look great" is not a sales pitch.&lt;/p&gt;

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

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Frontend&lt;/strong&gt;: uni-app + Vue 3 + TypeScript + Vite, exported to H5 plus WeChat mini-program from one codebase.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Hosting&lt;/strong&gt;: Cloudflare Pages, with Pages Functions for the API.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Payments&lt;/strong&gt;: Creem as merchant of record. Webhook verified with HMAC-SHA256, order rows in D1, idempotent on replay, amount checked against the SKU before anything is marked paid.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;PDF&lt;/strong&gt;: my own layout engine emits page geometry, then PyMuPDF embeds the CJK fonts. I started with pdf-lib and gave up on its CJK embedding — the output was not reliable enough for something a printer will actually run.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Four things I got wrong first
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;1. "Print-ready" is not "looks nice on screen."&lt;/strong&gt; Margins, bleed, and where a page break lands mid-sentence are the whole job. I rebuilt the layout around fixed print geometry after seeing the first proof.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. CJK fonts are the real weight problem.&lt;/strong&gt; A full GBK font is ~18MB; a subset is ~5.6MB. On a cold page load that is the difference between "fine" and "why is nothing happening."&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. I chased indexing too early.&lt;/strong&gt; The domain was five days old. I burned hours on Search Console dashboards when the honest answer was "wait two weeks." New domains get crawled on the crawler's schedule, not mine.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;4. Payments are the easy part now.&lt;/strong&gt; With a merchant of record, tax and card handling are somebody else's problem. The hard part is that nobody arrives to pay.&lt;/p&gt;

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

&lt;p&gt;Payments are live and verified end to end. Indexing is slowly happening. Traffic is the actual unsolved problem, and it is a marketing problem, not an engineering one.&lt;/p&gt;

&lt;p&gt;If you have a photo pile and a story attached to it, the first five pages cost nothing to look at: &lt;strong&gt;&lt;a href="https://pianyubook.com" rel="noopener noreferrer"&gt;https://pianyubook.com&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Feedback on the layout engine is very welcome — especially from anyone who has shipped a print PDF that a real printer accepted without complaining.&lt;/p&gt;

</description>
      <category>uniapp</category>
      <category>cloudflarechallenge</category>
      <category>sideprojects</category>
      <category>showdev</category>
    </item>
  </channel>
</rss>
