<?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: Takeshi Fuchi</title>
    <description>The latest articles on DEV Community by Takeshi Fuchi (@takeshi_fuchi).</description>
    <link>https://dev.to/takeshi_fuchi</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%2F1930996%2F4377278e-9133-45dd-8989-adfef8f9fd60.png</url>
      <title>DEV Community: Takeshi Fuchi</title>
      <link>https://dev.to/takeshi_fuchi</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/takeshi_fuchi"/>
    <language>en</language>
    <item>
      <title>I Built a Service That Actually Converts PDFs to Markdown Correctly</title>
      <dc:creator>Takeshi Fuchi</dc:creator>
      <pubDate>Tue, 02 Jun 2026 05:25:07 +0000</pubDate>
      <link>https://dev.to/takeshi_fuchi/i-built-a-service-that-actually-converts-pdfs-to-markdown-correctly-3bj4</link>
      <guid>https://dev.to/takeshi_fuchi/i-built-a-service-that-actually-converts-pdfs-to-markdown-correctly-3bj4</guid>
      <description>&lt;p&gt;Have you ever copy-pasted from a PDF only to get mangled line breaks, tables collapsed into a single line, formulas turned into gibberish, and figure captions floating somewhere completely wrong?&lt;/p&gt;

&lt;p&gt;You want to summarize a PDF with an LLM, organize old papers in Notion, or dump internal docs into a knowledge base — the goal is simple. But the moment you hit "PDF text extraction," everything falls apart before you even start.&lt;/p&gt;

&lt;p&gt;So I built &lt;strong&gt;&lt;a href="https://pdfmd.net" rel="noopener noreferrer"&gt;pdfmd.net&lt;/a&gt;&lt;/strong&gt; — upload a PDF, get back a properly structured Markdown file with headings, paragraphs, tables, LaTeX formulas, and figure references all intact.&lt;/p&gt;




&lt;p&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.amazonaws.com%2Fuploads%2Farticles%2Fex6fomkt5p24tokvd055.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.amazonaws.com%2Fuploads%2Farticles%2Fex6fomkt5p24tokvd055.png" alt="comparison" width="799" height="444"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  "Why not just attach the PDF to GPT-5.5?"
&lt;/h2&gt;

&lt;p&gt;Fair question. For a 1–2 page document, that works fine. Here's where the approaches differ:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;① Text Extraction Tools&lt;/th&gt;
&lt;th&gt;② GPT-5.5 / Claude directly&lt;/th&gt;
&lt;th&gt;③ pdfmd.net&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;How it works&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Reads character coordinates from PDF internals&lt;/td&gt;
&lt;td&gt;Attach PDF, ask "convert to Markdown"&lt;/td&gt;
&lt;td&gt;Page PNG → tuned LLM → Markdown + images as ZIP&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Speed / Cost&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;✅ Fast, free&lt;/td&gt;
&lt;td&gt;❌ High-end models get expensive fast&lt;/td&gt;
&lt;td&gt;✅ Cheap model by default, you choose&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Structure preserved&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;❌ Tables collapse, formulas break, 2-column layout mixes&lt;/td&gt;
&lt;td&gt;✅ Mostly, but varies with prompt&lt;/td&gt;
&lt;td&gt;✅ Consistent every time&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Long documents&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;✅ No page limit&lt;/td&gt;
&lt;td&gt;❌ Quality drops past ~50 pages&lt;/td&gt;
&lt;td&gt;✅ 200+ pages handled reliably&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Images / figures&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;❌ Zero information&lt;/td&gt;
&lt;td&gt;⚠️ Possible but you have to engineer it&lt;/td&gt;
&lt;td&gt;✅ Extracted and linked in ZIP automatically&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Batch processing&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;❌ One file at a time, manually&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The real gap with "just using GPT-5.5" is that &lt;strong&gt;you have to re-engineer it every time&lt;/strong&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Prompt required every time&lt;/strong&gt; — "format tables in Markdown", "use LaTeX for formulas", "extract figures with filenames", "bundle as ZIP" — skip any of these and output quality varies unpredictably.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Long documents degrade&lt;/strong&gt; — 50–200 page papers hit context limits, get cut off, or have visibly worse quality in the second half.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;One file at a time&lt;/strong&gt; — converting 10 papers means 10 upload-ask-copy cycles.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cost adds up&lt;/strong&gt; — processing large volumes through GPT-5.5 / Claude Sonnet class models gets expensive quickly.&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  How pdfmd works
&lt;/h2&gt;

&lt;p&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.amazonaws.com%2Fuploads%2Farticles%2F2ftx8j5ais3hpf2j0q8l.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.amazonaws.com%2Fuploads%2Farticles%2F2ftx8j5ais3hpf2j0q8l.png" alt="pdfmd pipeline" width="800" height="293"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;For each page, pdfmd extracts two things:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;PNG image&lt;/strong&gt; (full-page render) → primary input to the Vision LLM&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Text&lt;/strong&gt; (via PyMuPDF) → hints to help the LLM read characters accurately&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The Vision LLM uses the image as the &lt;strong&gt;ground truth for structure&lt;/strong&gt; and the text as a supplement. This is why 2-column layouts don't bleed into each other, tables stay intact, and formulas come out as LaTeX — structure is understood visually, not guessed from character positions.&lt;/p&gt;

&lt;p&gt;Figures and graphs are cropped from the page, saved as image files, and embedded in the Markdown as &lt;code&gt;![caption](./images/page3_fig1.png)&lt;/code&gt;. The final output is a &lt;strong&gt;ZIP containing the .md file and all images&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;On models and cost:&lt;/strong&gt; pdfmd runs on your own API key. You choose the model; API costs go directly to the provider. The default is a Gemini Flash Lite-class model — heavily tuned prompts and pipeline squeeze high-quality output from cheap models. Switch to a more capable model anytime when precision matters. This — a tuned pipeline running on your key — is the fundamental difference from asking GPT-5.5 directly.&lt;/p&gt;

&lt;p&gt;Just upload. A 200-page paper produces the same quality result every time, no prompt crafting required.&lt;/p&gt;




&lt;h2&gt;
  
  
  Seeing it in action
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Complex formulas: raw extraction vs. pdfmd
&lt;/h3&gt;

&lt;p&gt;Using a machine learning paper (&lt;a href="https://arxiv.org/abs/1906.05596" rel="noopener noreferrer"&gt;Conditional GAN, arxiv:1906.05596&lt;/a&gt;) as the test case.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Eq. 1 — GAN minimax objective&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Here's what the formula looks like in the PDF:&lt;/p&gt;

&lt;p&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.amazonaws.com%2Fuploads%2Farticles%2Fxe51wupx9ujfidluh8f8.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.amazonaws.com%2Fuploads%2Farticles%2Fxe51wupx9ujfidluh8f8.png" alt="Formula Eq.1 in the PDF" width="799" height="90"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Text extraction shatters it across lines. pdfmd reconstructs it as complete LaTeX:&lt;/p&gt;

&lt;p&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.amazonaws.com%2Fuploads%2Farticles%2Fq4omyoqwqe3twgekm0ur.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.amazonaws.com%2Fuploads%2Farticles%2Fq4omyoqwqe3twgekm0ur.png" alt="❌ Text extraction vs ✅ pdfmd output (Eq.1)" width="800" height="540"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;code&gt;\\mathbb{E}&lt;/code&gt; (expectation), bold vector &lt;code&gt;\\mathbf{x}&lt;/code&gt;, nested brackets — all recovered exactly. Text extraction can't tell &lt;code&gt;min&lt;/code&gt; from a subscript or an equation number from a line break.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;Eq. 9 — Piecewise (cases) function&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;In the PDF:&lt;/p&gt;

&lt;p&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.amazonaws.com%2Fuploads%2Farticles%2Ff270b5ypnq9etqmf0b6h.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.amazonaws.com%2Fuploads%2Farticles%2Ff270b5ypnq9etqmf0b6h.png" alt="Formula Eq.9 in the PDF" width="798" height="114"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Text extraction leaves bare brackets with misaligned text. pdfmd uses &lt;code&gt;\\begin{cases}&lt;/code&gt;:&lt;/p&gt;

&lt;p&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.amazonaws.com%2Fuploads%2Farticles%2Fp7fdzy2i1slruyqqidl3.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.amazonaws.com%2Fuploads%2Farticles%2Fp7fdzy2i1slruyqqidl3.png" alt="❌ Text extraction vs ✅ pdfmd output (Eq.9)" width="800" height="375"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Paste this into Obsidian, Notion, or a Jupyter notebook and the formula renders correctly.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;Eq. 5 — Fraction + Frobenius norm&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Compound expressions with fractions, multi-level subscripts, and norm notation:&lt;/p&gt;

&lt;p&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.amazonaws.com%2Fuploads%2Farticles%2Fbc88acc5f32u24khu11h.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.amazonaws.com%2Fuploads%2Farticles%2Fbc88acc5f32u24khu11h.png" alt="❌ Text extraction vs ✅ pdfmd output (Eq.5)" width="800" height="375"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;code&gt;\\frac{1}{WH}&lt;/code&gt;, subscript &lt;code&gt;\\theta_G&lt;/code&gt;, Frobenius norm &lt;code&gt;\\|\\cdot\\|_F^2&lt;/code&gt;, &lt;code&gt;\\Omega_{256}&lt;/code&gt; with subscripts — all correct. Text extraction splits numerator and denominator onto separate lines and drops the norm delimiters entirely.&lt;/p&gt;




&lt;h3&gt;
  
  
  Tables: subscripts intact
&lt;/h3&gt;

&lt;p&gt;Using an ADC datasheet (LTC2228/2227/2226) as the example. Electrical spec tables are full of subscripted symbols: &lt;code&gt;VCM&lt;/code&gt;, &lt;code&gt;VIH&lt;/code&gt;, &lt;code&gt;IIN&lt;/code&gt;, &lt;code&gt;IOUT&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The original PDF page:&lt;/p&gt;

&lt;p&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.amazonaws.com%2Fuploads%2Farticles%2F53p2ma3pn712optnipel.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.amazonaws.com%2Fuploads%2Farticles%2F53p2ma3pn712optnipel.png" alt="ADC datasheet original PDF" width="800" height="1035"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Text extraction strips subscripts: &lt;code&gt;VCM&lt;/code&gt;, &lt;code&gt;VIH&lt;/code&gt;, &lt;code&gt;IIN&lt;/code&gt; stay as plain text. pdfmd renders them all as proper LaTeX:&lt;/p&gt;

&lt;p&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.amazonaws.com%2Fuploads%2Farticles%2Fr9gisvoqrnproe1lqdyw.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.amazonaws.com%2Fuploads%2Farticles%2Fr9gisvoqrnproe1lqdyw.png" alt="❌ Text extraction vs ✅ pdfmd output (ADC datasheet table)" width="800" height="379"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;code&gt;VCM&lt;/code&gt; → $V_{CM}$, &lt;code&gt;VIH&lt;/code&gt; → $V_{IH}$, &lt;code&gt;IIN&lt;/code&gt; → $I_{IN}$, &lt;code&gt;IOUT = 0&lt;/code&gt; → $I_{OUT} = 0$.&lt;/p&gt;

&lt;p&gt;In a hardware spec, &lt;code&gt;VCM&lt;/code&gt; and $V_{CM}$ mean the same thing to a human but are different strings to any downstream system — search, LLM context, RAG retrieval. The subscript isn't cosmetic; it's semantic.&lt;/p&gt;




&lt;h3&gt;
  
  
  Page breaks + figure interruptions: text reconnected
&lt;/h3&gt;

&lt;p&gt;Using a 2-column academic paper from SIGMOD'18 (GPU parallel top-k algorithm). A paragraph spans two pages with a figure inserted mid-sentence.&lt;/p&gt;

&lt;p&gt;The source PDF (two pages stitched):&lt;/p&gt;

&lt;p&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.amazonaws.com%2Fuploads%2Farticles%2Fi0ijiovt8gf7hg96pn1p.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.amazonaws.com%2Fuploads%2Farticles%2Fi0ijiovt8gf7hg96pn1p.png" alt="Two-column PDF spanning a page break" width="800" height="1094"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Text extraction output:&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The paragraph is mid-sentence when the page footer fires:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;…This results in two bitonic sequences,
(S[0], ..S[l/2 −1]) and (S[l/2], ...S[l −1])
where all the elements in the first subsequence
are smaller than any element in the second
subsequence.
Research 15: Databases for Emerging Hardware     ← page footer
SIGMOD'18, June 10-15, 2018, Houston, TX, USA   ← session header
1558                                             ← page number
Phase  Step
1      1
2      1  2  3  4                                ← figure data
(a) Algorithm.
Unsorted Input
After Phase 1
…
In the second step, the same procedure           ← paragraph resumes here
is applied to both the subsequences…
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;pdfmd output:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;…This results in two bitonic sequences, $(S[0], &lt;span class="se"&gt;\\&lt;/span&gt;dots S[l/2 - 1])$ and
$(S[l/2], &lt;span class="se"&gt;\\&lt;/span&gt;dots S[l - 1])$ where all the elements in the first subsequence
are smaller than any element in the second subsequence. In the second step,
the same procedure is applied to both the subsequences, resulting in four
bitonic sequences…

&lt;span class="p"&gt;![&lt;/span&gt;&lt;span class="nv"&gt;Bitonic Sorting Network&lt;/span&gt;&lt;span class="p"&gt;](&lt;/span&gt;&lt;span class="sx"&gt;./images/gputopk_sigmod18_page3_fig1.png&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="gs"&gt;**Figure 3:**&lt;/span&gt; Bitonic Sorting Network
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&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.amazonaws.com%2Fuploads%2Farticles%2Fmggij7xzdsb5afkbt4yb.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.amazonaws.com%2Fuploads%2Farticles%2Fmggij7xzdsb5afkbt4yb.png" alt="❌ Text extraction vs ✅ pdfmd output (page break + figure)" width="800" height="402"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Page footer, session header, page number, and raw figure data all stripped. Paragraph text flows continuously. Figure placed at the right position with a proper reference.&lt;/p&gt;




&lt;h3&gt;
  
  
  Complex multi-column Japanese layout
&lt;/h3&gt;

&lt;p&gt;Multi-column Japanese documents — municipal newsletters, magazine spreads, textbooks mixing vertical and horizontal text — are the hardest layout class for text extraction. Columns bleed into each other, vertical text comes out one character per line, and decorative borders become garbage characters.&lt;/p&gt;

&lt;p&gt;Here's a one-page municipal newsletter put through pdfmd:&lt;/p&gt;

&lt;p&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.amazonaws.com%2Fuploads%2Farticles%2Fy6a4u1gtai4zkm53cfzg.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.amazonaws.com%2Fuploads%2Farticles%2Fy6a4u1gtai4zkm53cfzg.png" alt="Municipal newsletter original PDF" width="800" height="1189"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;PyMuPDF text extraction output:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;まちだ市民大学ＨＡＴＳ
まちだ市民大学ＨＡＴＳ
まちだ市民大学ＨＡＴＳ      ← heading repeated 3× (three columns)
受講生募集
襖
鴬                           ← decorative characters garbled
横横横横横横横横横横横横横横  ← border lines as characters

対市内在住、在勤、在学の、原則、全回出席できる方
場陶芸に関する講座＝陶芸スタジオ（下小山田町）、

日９月１０日～１２月１７日の月曜日     ← different section mixed in
人間関係学～人間関係の多様性と向き合   ← reading order scrambled

玉                           ← vertical text: one char per line
川
学
園
子
ど
も
…
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;pdfmd output:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;&lt;span class="gu"&gt;## 催し ご参加を&lt;/span&gt;

&lt;span class="gu"&gt;### 玉川学園子どもクラブ ころころ児童館&lt;/span&gt;

&lt;span class="gu"&gt;#### 【7月のわくわくWeek「水鉄砲合戦～広場決戦の巻」】&lt;/span&gt;
自分の水鉄砲を持ってきて参加できます。びしょぬれになるので、
着替えが必要な方はお持ち下さい。
&lt;span class="p"&gt;*&lt;/span&gt;   &lt;span class="gs"&gt;**対**&lt;/span&gt; 小学生以上の方
&lt;span class="p"&gt;*&lt;/span&gt;   &lt;span class="gs"&gt;**日**&lt;/span&gt; 7月23日(月)～8月3日(金)、いずれも午後3時30分～5時(雨天中止)

&lt;span class="gu"&gt;### まちだ市民大学HATS 2012年度後期講座 受講生募集&lt;/span&gt;
&lt;span class="p"&gt;
*&lt;/span&gt;   &lt;span class="gs"&gt;**対**&lt;/span&gt; 市内在住、在勤、在学の、原則、全回出席できる方
&lt;span class="p"&gt;*&lt;/span&gt;   &lt;span class="gs"&gt;**申**&lt;/span&gt; 7月25日正午～8月24日に電話でイベントダイヤル(📞724・5656)へ。
&lt;span class="p"&gt;*&lt;/span&gt;   &lt;span class="gs"&gt;**問**&lt;/span&gt; 生涯学習センター 📞728・0071 FAX728・0073
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Vertical text reconstructed, 3-column layout correctly ordered, &lt;code&gt;##&lt;/code&gt; → &lt;code&gt;###&lt;/code&gt; → &lt;code&gt;####&lt;/code&gt; heading hierarchy preserved, phone numbers and labels intact.&lt;/p&gt;




&lt;h3&gt;
  
  
  Multilingual: works the same way
&lt;/h3&gt;

&lt;p&gt;Because the approach is Vision LLM-based, language is not a special case. Here's a Chinese academic paper:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;&lt;span class="gh"&gt;# CUDA 并行计算技术在情报信息研判中的应用&lt;/span&gt;

&lt;span class="gs"&gt;**摘要：**&lt;/span&gt; 文章在研究公安情报信息研判技术的基础上…

$$W_{ik} = &lt;span class="se"&gt;\\&lt;/span&gt;frac{tf_{ik} &lt;span class="se"&gt;\\&lt;/span&gt;log(N/n_k + 0.01)}{&lt;span class="se"&gt;\\&lt;/span&gt;sqrt{&lt;span class="se"&gt;\\&lt;/span&gt;sum_{k=1}^n [tf_{ik} &lt;span class="se"&gt;\\&lt;/span&gt;log(N/n_k + 0.01)]^2}} &lt;span class="se"&gt;\\&lt;/span&gt;tag{1}$$
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Supported languages: &lt;strong&gt;English, Japanese, Simplified Chinese, Traditional Chinese, Spanish, French, Portuguese, Russian, German, Turkish, Korean, Italian, Dutch&lt;/strong&gt; — and the UI is translated into all of them.&lt;/p&gt;




&lt;h3&gt;
  
  
  Figures and graphs described, not just extracted
&lt;/h3&gt;

&lt;p&gt;For charts and graphs — content that can't be extracted as text — pdfmd saves the image and writes a caption describing what it shows:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;&lt;span class="p"&gt;![&lt;/span&gt;&lt;span class="nv"&gt;Boxplots of Dice scores&lt;/span&gt;&lt;span class="p"&gt;](&lt;/span&gt;&lt;span class="sx"&gt;./images/page7_fig2.png&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="gs"&gt;**Fig. 5.**&lt;/span&gt; Boxplots of Dice scores for various anatomical structures for ANTs,
NiftyReg, and VoxelMorph. Structures are ordered by average ANTs Dice score.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Ask an LLM "what does Fig. 5 show?" later, and it can actually answer — because the description is in the Markdown alongside the image reference.&lt;/p&gt;




&lt;h2&gt;
  
  
  When to use it
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Moving papers, contracts, or internal docs into &lt;strong&gt;Notion or Obsidian&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Preprocessing PDFs as &lt;strong&gt;RAG source documents&lt;/strong&gt; for LLM pipelines&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Quoting from a PDF&lt;/strong&gt; in a blog post or report — paste the Markdown directly&lt;/li&gt;
&lt;li&gt;Cleaning up multilingual material &lt;strong&gt;before sending to DeepL or GPT&lt;/strong&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you just need text search inside a PDF, a viewer does that fine. pdfmd is for when you need to &lt;strong&gt;do something with the Markdown afterward&lt;/strong&gt;.&lt;/p&gt;




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

&lt;p&gt;&lt;strong&gt;&lt;a href="https://pdfmd.net" rel="noopener noreferrer"&gt;pdfmd.net&lt;/a&gt;&lt;/strong&gt; — sign up and get &lt;strong&gt;50 pages free&lt;/strong&gt;. 1 point = 1 page. Upload a file, wait a moment, download the ZIP. That's it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The API key is free too.&lt;/strong&gt; pdfmd runs on your own API key, but &lt;a href="https://aistudio.google.com/" rel="noopener noreferrer"&gt;Google AI Studio&lt;/a&gt; lets you generate a Gemini API key at no cost. The default model (Gemini Flash Lite class) runs within AI Studio's free tier — so those 50 signup pages are &lt;strong&gt;completely free end-to-end&lt;/strong&gt;, API costs included. No credit card required anywhere.&lt;/p&gt;

&lt;p&gt;If you've ever fed a badly-extracted PDF into an LLM and gotten back a confused or hallucinated answer, try running the same PDF through pdfmd first. The difference is usually immediate.&lt;/p&gt;

</description>
      <category>pdf</category>
      <category>markdown</category>
      <category>ai</category>
    </item>
    <item>
      <title>I Built a VS Code Extension from Scratch with Claude Code — and Got 10+ Downloads Within the First Hour of Publishing</title>
      <dc:creator>Takeshi Fuchi</dc:creator>
      <pubDate>Fri, 29 May 2026 17:56:46 +0000</pubDate>
      <link>https://dev.to/takeshi_fuchi/i-built-a-vs-code-extension-from-scratch-with-claude-code-and-got-10-downloads-within-the-first-35mc</link>
      <guid>https://dev.to/takeshi_fuchi/i-built-a-vs-code-extension-from-scratch-with-claude-code-and-got-10-downloads-within-the-first-35mc</guid>
      <description>&lt;h2&gt;
  
  
  Introduction
&lt;/h2&gt;

&lt;p&gt;"I just want to peek inside this zip file without actually extracting it." — Every developer has been there. But VS Code's default behavior is either showing raw binary or launching an external app. Unzip, check, delete the folder — that friction adds up. I built this extension to eliminate it.&lt;/p&gt;

&lt;p&gt;The entire project was developed through conversation with Claude Code, starting from zero. I also wired in an ad-based monetization model. Then I published it to the VS Code Marketplace without any promotion whatsoever — and watched 10+ downloads roll in within the first hour.&lt;/p&gt;

&lt;p&gt;Here's the full story.&lt;/p&gt;




&lt;h2&gt;
  
  
  What Is "Zip &amp;amp; Archive Viewer"?
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Zip &amp;amp; Archive Viewer&lt;/strong&gt; (publisher: Takeshi-Fuchi) is a VS Code extension that lets you &lt;strong&gt;browse and preview the contents of archive files directly in the editor — no extraction required&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Select a zip file and it immediately renders as a file tree. Mouse down on any filename in the list to preview its contents inline; release to close. No temp folders, no cleanup. For the job of "just checking what's inside," this is the shortest possible path.&lt;/p&gt;

&lt;h3&gt;
  
  
  Supported Formats
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;.zip&lt;/code&gt; / &lt;code&gt;.7z&lt;/code&gt; / &lt;code&gt;.tar&lt;/code&gt; / &lt;code&gt;.tar.gz&lt;/code&gt; / &lt;code&gt;.tgz&lt;/code&gt; / &lt;code&gt;.tar.xz&lt;/code&gt; / &lt;code&gt;.tar.bz2&lt;/code&gt; / &lt;code&gt;.tar.zst&lt;/code&gt; — 17 compression formats in total, including modern ones like Zstandard.&lt;/p&gt;

&lt;h3&gt;
  
  
  Key Features
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;File Tree View&lt;/strong&gt;&lt;br&gt;
Selecting an archive displays filenames, sizes, and timestamps in a collapsible tree.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Instant File Preview (No Extraction)&lt;/strong&gt;&lt;br&gt;
Hold the mouse button down on any filename to preview its content; release to dismiss. Shows the first N lines of text (configurable, default 20). Nothing is extracted to disk.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Image Preview (No Extraction)&lt;/strong&gt;&lt;br&gt;
JPG / PNG / GIF / WebP / BMP / SVG / ICO / TIFF / AVIF render inline inside the viewer. See exactly what's in the archive without touching your filesystem.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Markdown Preview (No Extraction)&lt;/strong&gt;&lt;br&gt;
Rendered Markdown is displayed directly from within the archive. Images referenced inside the archive are automatically resolved and embedded. Great for checking documentation in a release package.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Nested Archive Browsing&lt;/strong&gt;&lt;br&gt;
A zip inside a tar.gz? Click through and browse it. Still no extraction needed.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Password-Protected Archives&lt;/strong&gt;&lt;br&gt;
ZIP and 7Z password-protected archives are fully supported. Passwords are remembered for the session.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Selective Extraction&lt;/strong&gt;&lt;br&gt;
Once you've confirmed what you need, right-click to extract individual files or entire folders. Multi-select with checkboxes for batch extraction. Extraction is an intentional step — not the default.&lt;/p&gt;




&lt;h2&gt;
  
  
  Building It from Scratch with Claude Code
&lt;/h2&gt;

&lt;h3&gt;
  
  
  How It Started
&lt;/h3&gt;

&lt;p&gt;One day I received a zip file, needed to quickly verify its contents, and found myself going through the whole unzip → check → delete cycle just for a 30-second confirmation. "This should just work inside VS Code," I thought.&lt;/p&gt;

&lt;p&gt;I looked for existing extensions. Some were unmaintained, some only supported zip, some had clunky UIs. Close enough didn't cut it, so I decided to build one. The catch: I had never built a VS Code extension before. The Webview API, CustomEditorProvider, extension context lifecycle — unfamiliar territory. That's where Claude Code came in.&lt;/p&gt;

&lt;h3&gt;
  
  
  How Development Progressed
&lt;/h3&gt;

&lt;p&gt;The workflow was straightforward. I described what I wanted — "show a tree view when a zip file is selected in VS Code, without extracting it" — and Claude Code generated the scaffolding. From there I layered in features: "add 7z support," "handle password-protected archives," "render Markdown from inside the archive."&lt;/p&gt;

&lt;p&gt;The main file, &lt;code&gt;src/extension.ts&lt;/code&gt;, is about 2,000 lines. Stream-based reading for each archive format, HTML generation for the Webview, preview logic, password dialogs, batch extraction — all in one file.&lt;/p&gt;

&lt;p&gt;I wrote tests too: unit tests, integration tests against real archive files, and end-to-end tests with Playwright. When something broke, I'd pass the failure back — "this test is failing, find out why" — and get a precise fix. The debug cycle was noticeably faster than working alone.&lt;/p&gt;

&lt;p&gt;Looking at the git history: from &lt;code&gt;Initial commit&lt;/code&gt; through feature additions, bug fixes, and version bumps, the entire arc from zero to published was one developer in conversation with an AI.&lt;/p&gt;

&lt;h3&gt;
  
  
  What I Took Away from the Experience
&lt;/h3&gt;

&lt;p&gt;The biggest win with Claude Code was not having to look up unfamiliar APIs. Things like using &lt;code&gt;vscode.window.showInputBox()&lt;/code&gt; for a password prompt, or &lt;code&gt;retainContextWhenHidden&lt;/code&gt; to preserve Webview state across tab switches — I could just describe what I wanted and get working code, without context-switching to docs.&lt;/p&gt;

&lt;p&gt;Not everything worked on the first try. Images broke due to a Content Security Policy misconfiguration. The tar tree hierarchy got mangled at one point. Small bugs, real bugs. But the pattern of "here's the failing test, fix it" reliably produced accurate fixes. The time I spent debugging dropped significantly.&lt;/p&gt;




&lt;h2&gt;
  
  
  Monetizing a VS Code Extension with Ads
&lt;/h2&gt;

&lt;p&gt;Before publishing, I added something unusual: &lt;strong&gt;designated ad space within the extension UI&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;The README states it plainly:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;This extension may display advertisements in designated advertising spaces within the interface. Revenue from advertisements is used for the development, maintenance, and improvement of the extension.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;In-app advertising is standard in mobile apps and web services, but it's rare in developer tooling. The VS Code extension ecosystem is almost entirely donation-ware or subscription-based. I wanted to test whether an ad model could work here.&lt;/p&gt;

&lt;p&gt;Some pushback is expected — developers tend to have low tolerance for ads in their tools. The constraints I set for myself:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Non-intrusive&lt;/strong&gt;: Ads sit at the edge of the UI and don't interfere with archive browsing&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Fully local&lt;/strong&gt;: No file data is sent externally, stated explicitly in the privacy policy&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Transparent&lt;/strong&gt;: The README discloses ad presence before anyone installs&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Whether the developer community accepts this or not is the experiment. The results will tell me something either way.&lt;/p&gt;




&lt;h2&gt;
  
  
  10+ Downloads in the First Hour — Without Any Promotion
&lt;/h2&gt;

&lt;p&gt;I had zero expectations at launch. No tweets, no Reddit posts, no Product Hunt submission. I published to the VS Code Marketplace and that was it.&lt;/p&gt;

&lt;p&gt;Within an hour, downloads had crossed 10.&lt;/p&gt;

&lt;p&gt;I was genuinely surprised.&lt;/p&gt;

&lt;p&gt;In hindsight, it makes sense. The VS Code Marketplace has a massive, active user base searching for extensions every day. When you search "zip viewer" or "archive viewer" as a new listing, you surface at the top. No SEO work, no ad spend — the marketplace's own search index does the distribution.&lt;/p&gt;

&lt;p&gt;Launching a web service from scratch is a cold start problem. You need SEO, social posts, community engagement, and paid acquisition just to get visible. A marketplace is different. The audience is already there. The infrastructure connecting supply and demand already exists.&lt;/p&gt;

&lt;p&gt;The App Store and Google Play have the same structure, but the VS Code Marketplace is narrower: everyone visiting is a developer looking for a tool. The match rate between "I want to preview zip contents in VS Code" and this extension is near-perfect. A real solution to a real need gets found automatically.&lt;/p&gt;




&lt;h2&gt;
  
  
  Where It Is Now
&lt;/h2&gt;

&lt;p&gt;Current version: 2.1.2. It started at 1.0.0 with preview support for 5 ZIP/TAR formats, expanded to 17 formats with a major UI overhaul in 2.0.0, and added Markdown preview, image preview, and batch extraction in 2.1.0. The scope has grown considerably from what I originally had in mind.&lt;/p&gt;

&lt;p&gt;How the ad model performs, how downloads trend, how the developer community responds — I'll write a follow-up when there's something worth reporting.&lt;/p&gt;

&lt;p&gt;If you've ever caught yourself unzipping just to check a file and immediately deleting the folder, give it a try.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Search "Zip &amp;amp; Archive Viewer" by Takeshi-Fuchi on the VS Code Marketplace&lt;/strong&gt;&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Source code is available at &lt;a href="https://github.com/t-fuchi/zip-viewer" rel="noopener noreferrer"&gt;https://github.com/t-fuchi/zip-viewer&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

</description>
      <category>vscode</category>
      <category>extensions</category>
      <category>zip</category>
      <category>preview</category>
    </item>
  </channel>
</rss>
