@ammduncan/easel 0.2.14 → 0.2.15

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -2,6 +2,13 @@
2
2
 
3
3
  All notable changes to easel. This project adheres to [Semantic Versioning](https://semver.org/).
4
4
 
5
+ ## 0.2.15 — 2026-05-23
6
+
7
+ ### Fixed
8
+ - **PDF exports were enormous (300+ MB for a single-page card).** Two stacking causes: (1) the iframe always rasterised at `pixelRatio: 4` regardless of target, producing huge bitmaps for tall cards; (2) the parent then embedded the result into jsPDF as a `PNG`, which PDFs store using Flate compression — far less efficient than the DCT compression PDFs natively use for JPEGs. A tall card at DPR 4 → ~6000×10000 pixel PNG → ~300 MB PDF wrapper.
9
+ - Fix: for PDF targets only, the iframe now rasterises as JPEG at `quality: 0.92` and `pixelRatio: 2`, and the parent embeds with `'JPEG'` format + `'FAST'` compression flag + `compress: true` at the document level. PNG exports stay at lossless PNG + pixelRatio 4 — no quality loss for the standalone PNG download.
10
+ - Expected sizes for a typical card: ~3–8 MB (down from ~300 MB), text still crisp on screen and in print.
11
+
5
12
  ## 0.2.14 — 2026-05-23
6
13
 
7
14
  ### Changed
@@ -196,8 +196,12 @@
196
196
  }
197
197
 
198
198
  /**
199
- * Embed a PNG dataURL into a single-page PDF sized to the image's pixel
200
- * dimensions, producing a continuous (no page-breaks) document, then save.
199
+ * Embed a rasterised dataURL into a single-page PDF sized to the image's
200
+ * pixel dimensions, producing a continuous (no page-breaks) document, then
201
+ * save. The iframe sends a JPEG dataURL for PDF targets — PDFs natively use
202
+ * DCT compression for JPEGs, so embedding stays compact (vs PNGs which
203
+ * balloon the file). We detect format from the dataURL prefix; PNG still
204
+ * works as a fallback for any legacy caller that sends one.
201
205
  */
202
206
  function downloadAsPdf(dataUrl, filename) {
203
207
  return new Promise((resolve, reject) => {
@@ -206,6 +210,8 @@
206
210
  reject(new Error("jsPDF not loaded"));
207
211
  return;
208
212
  }
213
+ const isJpeg = dataUrl.startsWith("data:image/jpeg") || dataUrl.startsWith("data:image/jpg");
214
+ const imageType = isJpeg ? "JPEG" : "PNG";
209
215
  const img = new Image();
210
216
  img.onload = () => {
211
217
  try {
@@ -216,8 +222,9 @@
216
222
  format: [w, h],
217
223
  orientation: w > h ? "landscape" : "portrait",
218
224
  hotfixes: ["px_scaling"],
225
+ compress: true,
219
226
  });
220
- pdf.addImage(dataUrl, "PNG", 0, 0, w, h);
227
+ pdf.addImage(dataUrl, imageType, 0, 0, w, h, undefined, "FAST");
221
228
  pdf.save(filename);
222
229
  resolve();
223
230
  } catch (err) {
@@ -859,13 +866,20 @@ ${body}
859
866
  document.documentElement.scrollHeight,
860
867
  document.body ? document.body.scrollHeight : 0,
861
868
  );
862
- window.htmlToImage.toPng(document.documentElement, {
869
+ // PNG target → lossless PNG @ pixelRatio 4 for crisp standalone files.
870
+ // PDF target → JPEG @ quality 0.92 + pixelRatio 2. PDFs natively use
871
+ // DCT compression for embedded images, so JPEG-in-PDF stays small;
872
+ // PNG-in-PDF balloons (a tall card at DPR 4 produces 300+ MB PDFs).
873
+ var rasterFn = format === "pdf" ? window.htmlToImage.toJpeg : window.htmlToImage.toPng;
874
+ var rasterOpts = {
863
875
  backgroundColor: bgColor,
864
- pixelRatio: 4,
876
+ pixelRatio: format === "pdf" ? 2 : 4,
865
877
  cacheBust: true,
866
878
  width: width,
867
879
  height: height,
868
- }).then(function(dataUrl){
880
+ };
881
+ if (format === "pdf") rasterOpts.quality = 0.92;
882
+ rasterFn(document.documentElement, rasterOpts).then(function(dataUrl){
869
883
  parent.postMessage({ type: "easel:image-ready", pushId: pushId, dataUrl: dataUrl, filename: filename, format: format }, "*");
870
884
  }).catch(function(err){
871
885
  console.error("[easel] export failed", err);
@@ -889,7 +903,7 @@ ${body}
889
903
  const configScript =
890
904
  "<script src='https://cdn.jsdelivr.net/npm/html-to-image@1.11.13/dist/html-to-image.js'></script><script>(function(){function a(c){if(!c)return;if(c.theme==='light'||c.theme==='dark'){document.documentElement.setAttribute('data-theme',c.theme);window.__claudeDisplayTheme=c.theme}if(c.preset==='paper'||c.preset==='aurora'||c.preset==='slate'){document.documentElement.setAttribute('data-preset',c.preset);window.__claudeDisplayPreset=c.preset}if(c.density==='carded'||c.density==='flat'){document.documentElement.setAttribute('data-density',c.density);window.__claudeDisplayDensity=c.density}}a(" +
891
905
  JSON.stringify({ theme, preset, density }) +
892
- ");window.addEventListener('message',function(e){if(!e||!e.data)return;if(e.data.type==='easel:config')a(e.data);if(e.data.type==='easel:theme')a({theme:e.data.theme});if(e.data.type==='easel:print'){try{window.print()}catch(_){}}if(e.data.type==='easel:image'){var pid=e.data.pushId;var fn=e.data.filename||'push.png';var fmt=e.data.format==='pdf'?'pdf':'png';var bg=e.data.bgColor||'#ffffff';if(!window.htmlToImage)return;window.htmlToImage.toPng(document.body,{backgroundColor:bg,pixelRatio:4,cacheBust:true}).then(function(u){parent.postMessage({type:'easel:image-ready',pushId:pid,dataUrl:u,filename:fn,format:fmt},'*')}).catch(function(err){console.error(err);parent.postMessage({type:'easel:image-error',pushId:pid,format:fmt,message:(err&&err.message)?err.message:String(err)},'*')})}})})();</script>";
906
+ ");window.addEventListener('message',function(e){if(!e||!e.data)return;if(e.data.type==='easel:config')a(e.data);if(e.data.type==='easel:theme')a({theme:e.data.theme});if(e.data.type==='easel:print'){try{window.print()}catch(_){}}if(e.data.type==='easel:image'){var pid=e.data.pushId;var fn=e.data.filename||'push.png';var fmt=e.data.format==='pdf'?'pdf':'png';var bg=e.data.bgColor||'#ffffff';if(!window.htmlToImage)return;var rfn=fmt==='pdf'?window.htmlToImage.toJpeg:window.htmlToImage.toPng;var ropts={backgroundColor:bg,pixelRatio:fmt==='pdf'?2:4,cacheBust:true};if(fmt==='pdf')ropts.quality=0.92;rfn(document.body,ropts).then(function(u){parent.postMessage({type:'easel:image-ready',pushId:pid,dataUrl:u,filename:fn,format:fmt},'*')}).catch(function(err){console.error(err);parent.postMessage({type:'easel:image-error',pushId:pid,format:fmt,message:(err&&err.message)?err.message:String(err)},'*')})}})})();</script>";
893
907
  const measureScript = "<script>" + selfMeasureScript(pushId) + "</script>";
894
908
  const combined = configScript + measureScript;
895
909
  if (/<\/body>/i.test(html)) return html.replace(/<\/body>/i, combined + "</body>");
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ammduncan/easel",
3
- "version": "0.2.14",
3
+ "version": "0.2.15",
4
4
  "description": "A live browser tab for every Claude Code (and MCP) session. The push MCP tool appends HTML cards to a scrolling feed you keep open in split-screen.",
5
5
  "type": "module",
6
6
  "license": "MIT",