<?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: darksun113</title>
    <description>The latest articles on DEV Community by darksun113 (@darksun113).</description>
    <link>https://dev.to/darksun113</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%2F3990372%2F984b15db-407b-4f0d-abe7-a34bd83756aa.jpg</url>
      <title>DEV Community: darksun113</title>
      <link>https://dev.to/darksun113</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/darksun113"/>
    <language>en</language>
    <item>
      <title>Render PowerPoint (.pptx) to HTML in pure JavaScript — no LibreOffice, no headless browser</title>
      <dc:creator>darksun113</dc:creator>
      <pubDate>Thu, 18 Jun 2026 07:10:50 +0000</pubDate>
      <link>https://dev.to/darksun113/render-powerpoint-pptx-to-html-in-pure-javascript-no-libreoffice-no-headless-browser-2921</link>
      <guid>https://dev.to/darksun113/render-powerpoint-pptx-to-html-in-pure-javascript-no-libreoffice-no-headless-browser-2921</guid>
      <description>&lt;p&gt;If you've ever had to turn a &lt;strong&gt;PowerPoint to HTML&lt;/strong&gt; on a server, you know the usual options are grim. You either shell out to &lt;strong&gt;LibreOffice&lt;/strong&gt; in headless mode (a 400 MB dependency that forks a process per file and occasionally hangs), spin up a headless browser, or ship the deck off to some cloud conversion API and pray about the privacy implications. For something that's &lt;em&gt;just a file format&lt;/em&gt;, that's a lot of moving parts.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;deck-ir&lt;/strong&gt; takes a different route. It does &lt;strong&gt;pptx to html&lt;/strong&gt; in pure &lt;strong&gt;JavaScript&lt;/strong&gt; — &lt;strong&gt;without LibreOffice&lt;/strong&gt;, without a headless browser, without native binaries, and without a single network call. It reads the OOXML inside the &lt;code&gt;.pptx&lt;/code&gt; directly and emits an HTML fragment per slide. It runs in Node.js, and because it has zero native dependencies, it also runs &lt;strong&gt;client-side&lt;/strong&gt; in the browser.&lt;/p&gt;

&lt;p&gt;It's the open-source rendering core extracted from the PPTX→HTML pipeline of a commercial product, &lt;a href="https://flashdeck.cn" rel="noopener noreferrer"&gt;flashdeck.cn&lt;/a&gt;. The whole thing leans on exactly two runtime dependencies: &lt;code&gt;jszip&lt;/code&gt; and &lt;code&gt;fast-xml-parser&lt;/code&gt;. No Chromium, no LLM, no storage layer.&lt;/p&gt;

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

&lt;p&gt;Before any of the explanation — here's the &lt;a href="https://darksun113.github.io/deck-ir/" rel="noopener noreferrer"&gt;live demo&lt;/a&gt;. Drop a &lt;code&gt;.pptx&lt;/code&gt; onto the page and it renders &lt;strong&gt;in your browser&lt;/strong&gt;. There's no upload; the parsing and rendering happen 100% client-side in JavaScript. That demo &lt;em&gt;is&lt;/em&gt; the library, which is the most honest proof I can offer that it doesn't secretly need a server.&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ftqaps87dbv14pek0u3i2.gif" 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%2Ftqaps87dbv14pek0u3i2.gif" alt="deck-ir live demo — drop a .pptx, render to HTML in the browser" width="720" height="509"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Why "no LibreOffice" actually matters
&lt;/h2&gt;

&lt;p&gt;The LibreOffice-in-a-container approach works, but it costs you:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A huge image and a heavyweight process you have to babysit, rate-limit, and restart.&lt;/li&gt;
&lt;li&gt;Nondeterminism — subprocess timeouts, font fallbacks that drift, the occasional zombie.&lt;/li&gt;
&lt;li&gt;All-or-nothing output. You get a rendered file, not a structured model you can post-process.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;deck-ir is &lt;strong&gt;deterministic&lt;/strong&gt;: the same &lt;code&gt;.pptx&lt;/code&gt; always produces the same HTML. And the output is &lt;strong&gt;self-contained&lt;/strong&gt; — media is inlined as &lt;code&gt;data:&lt;/code&gt; URLs by default, so each slide's HTML fragment stands alone with no asset directory to manage. (If you'd rather host media yourself, pass a &lt;code&gt;mediaResolver&lt;/code&gt; and deck-ir hands you the bytes instead.)&lt;/p&gt;

&lt;h2&gt;
  
  
  How it works: a .pptx is just a ZIP of XML
&lt;/h2&gt;

&lt;p&gt;The trick to &lt;strong&gt;convert pptx&lt;/strong&gt; without an office suite is realizing there's no magic in the format. A &lt;code&gt;.pptx&lt;/code&gt; is a ZIP archive of XML files (the OOXML spec). deck-ir unzips it with &lt;code&gt;jszip&lt;/code&gt;, parses the XML with &lt;code&gt;fast-xml-parser&lt;/code&gt;, and walks the tree itself.&lt;/p&gt;

&lt;p&gt;The pipeline has three stages:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;parsePptxToRawIR&lt;/code&gt;&lt;/strong&gt; — builds a faithful, low-level model of the OOXML. No interpretation yet, just structure.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;transformToSemanticIR&lt;/code&gt;&lt;/strong&gt; — this is where the real work happens. It resolves the things PowerPoint leaves implicit:

&lt;ul&gt;
&lt;li&gt;The &lt;strong&gt;3-layer background merge&lt;/strong&gt; (master → layout → slide), so each slide gets the background it actually displays.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Color schemes&lt;/strong&gt;, including &lt;code&gt;lumMod&lt;/code&gt;/&lt;code&gt;lumOff&lt;/code&gt; modulation resolved down to concrete hex values.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;EMU → px&lt;/strong&gt; conversion for absolute positioning (PowerPoint measures in English Metric Units; the browser doesn't).&lt;/li&gt;
&lt;li&gt;Text runs, geometry, groups, z-order, and &lt;strong&gt;SmartArt&lt;/strong&gt; reconstructed from its cached drawing.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;An HTML/SVG emitter&lt;/strong&gt; — turns the semantic model into a standalone HTML fragment per slide.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Because each stage is a clean transform over a data structure, you can tap in at any level instead of accepting whatever a black box hands you.&lt;/p&gt;

&lt;h2&gt;
  
  
  How to convert a .pptx to HTML in Node.js
&lt;/h2&gt;

&lt;p&gt;The common path is one call:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&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;parsePptx&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="s2"&gt;deck-ir&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;readFile&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="s2"&gt;node:fs/promises&lt;/span&gt;&lt;span class="dl"&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;buffer&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;readFile&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;deck.pptx&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;slideSize&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;slides&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;media&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;parsePptx&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;buffer&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;// slides: Array&amp;lt;{ html: string; warnings: string[] }&amp;gt;&lt;/span&gt;
&lt;span class="c1"&gt;//   one faithful HTML fragment per slide&lt;/span&gt;
&lt;span class="c1"&gt;// slideSize: the deck's dimensions&lt;/span&gt;
&lt;span class="c1"&gt;// media: the inlined assets (when you opt out of data: URLs)&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="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;i&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;slide&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="nx"&gt;slides&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;entries&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`Slide &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;i&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;:`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;slide&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;warnings&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="c1"&gt;// slide.html is ready to drop into a page&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That &lt;code&gt;buffer&lt;/code&gt; works the same in the browser — feed it the &lt;code&gt;ArrayBuffer&lt;/code&gt; from a &lt;code&gt;File&lt;/code&gt; and you get the same fragments, no server round-trip.&lt;/p&gt;

&lt;p&gt;If you want finer control, the lower-level pieces are exported too: &lt;code&gt;parsePptxToRawIR&lt;/code&gt;, &lt;code&gt;transformToSemanticIR&lt;/code&gt;, and &lt;code&gt;emitSlideHtml&lt;/code&gt;. Parse once, inspect or mutate the intermediate representation (hence the name — &lt;strong&gt;deck IR&lt;/strong&gt;), then emit. That's the seam most "render my slides" tools never give you.&lt;/p&gt;

&lt;h2&gt;
  
  
  What it renders
&lt;/h2&gt;

&lt;p&gt;deck-ir aims for faithful layout, not a loose approximation:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Shapes&lt;/strong&gt; — rectangles, ellipses, lines, and arbitrary custom geometry.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Fills&lt;/strong&gt; — solid, gradient, and image fills.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Outlines&lt;/strong&gt; — stroke color/width, plus border-radius.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Text&lt;/strong&gt; — font family, size, color, bold/italic/underline, alignment, and bullets.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Images&lt;/strong&gt; — including &lt;code&gt;srcRect&lt;/code&gt; cropping.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Backgrounds&lt;/strong&gt; — the full 3-layer master/layout/slide merge.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Color&lt;/strong&gt; — theme color schemes resolved to hex.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Structure&lt;/strong&gt; — z-order, groups, and SmartArt (from its cached drawing).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Positioning&lt;/strong&gt; — EMU→px absolute layout so things land where the author put them.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Honest limitations
&lt;/h2&gt;

&lt;p&gt;No converter is lossless, and pretending otherwise just wastes your afternoon. Here's what deck-ir does &lt;em&gt;not&lt;/em&gt; do:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Charts and tables without a cached drawing&lt;/strong&gt; fall back to a placeholder. (When PowerPoint has cached a drawing for them, they render; when it hasn't, there's nothing static to draw.)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Animations, transitions, audio, and video are ignored.&lt;/strong&gt; The output is static HTML — a faithful snapshot of each slide, not a player.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Fonts are not embedded.&lt;/strong&gt; Slides emit &lt;code&gt;font-family&lt;/code&gt; names only. That keeps the output small, but it means &lt;strong&gt;CJK text needs CJK fonts installed on the viewer's machine&lt;/strong&gt; to render correctly. Plan for that if your decks are Chinese, Japanese, or Korean.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;None of these are secret asterisks — they're the natural edges of "static, deterministic, dependency-free." If you need animations or pixel-perfect chart rendering, you genuinely do want a heavier tool.&lt;/p&gt;

&lt;h2&gt;
  
  
  A sibling for templating: deck-ir-vlm
&lt;/h2&gt;

&lt;p&gt;If you want to go beyond rendering, there's a companion library, &lt;strong&gt;deck-ir-vlm&lt;/strong&gt;, that adds a VLM layer on top of deck-ir to classify and cluster a deck's slides into editable HTML template layouts — text becomes &lt;code&gt;{{token}}&lt;/code&gt; slots you can fill programmatically. That's a different job (and it does pull in a model), so it's a separate package: &lt;a href="https://github.com/darksun113/deck-ir-vlm" rel="noopener noreferrer"&gt;github.com/darksun113/deck-ir-vlm&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Try it, install it, star it
&lt;/h2&gt;

&lt;p&gt;If you do anything with slide decks on the server — thumbnails, previews, search indexing, content extraction — deck-ir is worth ten minutes:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Live demo&lt;/strong&gt; (client-side, drop a &lt;code&gt;.pptx&lt;/code&gt;): &lt;a href="https://darksun113.github.io/deck-ir/" rel="noopener noreferrer"&gt;https://darksun113.github.io/deck-ir/&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Install:&lt;/strong&gt; &lt;code&gt;npm i deck-ir&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;GitHub&lt;/strong&gt; (a star helps it reach the next person fighting LibreOffice): &lt;a href="https://github.com/darksun113/deck-ir" rel="noopener noreferrer"&gt;https://github.com/darksun113/deck-ir&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;License:&lt;/strong&gt; AGPL-3.0, with a commercial dual-license available if AGPL doesn't fit your stack.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;And if you'd rather not build the rest of the slide pipeline yourself — AI one-line → full deck, an editor, export to &lt;em&gt;editable&lt;/em&gt; PPTX, mermaid support — that whole product lives at &lt;a href="https://flashdeck.cn" rel="noopener noreferrer"&gt;flashdeck.cn&lt;/a&gt;. deck-ir is the rendering core it's built on, now open for you to use directly.&lt;/p&gt;

</description>
      <category>javascript</category>
      <category>opensource</category>
      <category>showdev</category>
      <category>node</category>
    </item>
  </channel>
</rss>
