@merittdev/horus-lens 0.1.1 → 0.2.0

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/dist/index.cjs CHANGED
@@ -14633,15 +14633,86 @@ var TargetElementSchema = external_exports.object({
14633
14633
  boundingRect: external_exports.object({ x: external_exports.number(), y: external_exports.number(), width: external_exports.number(), height: external_exports.number() }).optional(),
14634
14634
  reactComponents: external_exports.array(external_exports.string()).optional()
14635
14635
  });
14636
- var AnnotationSchema = external_exports.object({
14637
- kind: external_exports.enum(["rect", "ellipse", "arrow"]),
14638
- x: external_exports.number(),
14639
- y: external_exports.number(),
14640
- width: external_exports.number(),
14641
- height: external_exports.number(),
14642
- /** Page coordinates relative to the viewport at capture time. */
14643
- viewport: external_exports.object({ width: external_exports.number(), height: external_exports.number() })
14644
- });
14636
+ var annotationCommon = {
14637
+ id: external_exports.string().optional(),
14638
+ /** Any CSS color string. */
14639
+ color: external_exports.string().optional(),
14640
+ strokeWidth: external_exports.number().optional()
14641
+ };
14642
+ var AnnotationPointSchema = external_exports.object({ x: external_exports.number(), y: external_exports.number() });
14643
+ var AnnotationSchema = external_exports.discriminatedUnion("kind", [
14644
+ external_exports.object({
14645
+ ...annotationCommon,
14646
+ kind: external_exports.literal("rect"),
14647
+ x: external_exports.number(),
14648
+ y: external_exports.number(),
14649
+ width: external_exports.number(),
14650
+ height: external_exports.number(),
14651
+ /**
14652
+ * OPTIONAL, kept for back-compat with 0.1.x clients that emit
14653
+ * viewport-anchored rects. New clients emit document-pixel rects with no
14654
+ * viewport.
14655
+ */
14656
+ viewport: external_exports.object({ width: external_exports.number(), height: external_exports.number() }).optional()
14657
+ }),
14658
+ external_exports.object({
14659
+ ...annotationCommon,
14660
+ kind: external_exports.literal("ellipse"),
14661
+ x: external_exports.number(),
14662
+ y: external_exports.number(),
14663
+ width: external_exports.number(),
14664
+ height: external_exports.number()
14665
+ }),
14666
+ external_exports.object({
14667
+ ...annotationCommon,
14668
+ kind: external_exports.literal("blur"),
14669
+ x: external_exports.number(),
14670
+ y: external_exports.number(),
14671
+ width: external_exports.number(),
14672
+ height: external_exports.number()
14673
+ }),
14674
+ external_exports.object({
14675
+ ...annotationCommon,
14676
+ kind: external_exports.literal("arrow"),
14677
+ x1: external_exports.number(),
14678
+ y1: external_exports.number(),
14679
+ x2: external_exports.number(),
14680
+ y2: external_exports.number()
14681
+ }),
14682
+ external_exports.object({
14683
+ ...annotationCommon,
14684
+ kind: external_exports.literal("line"),
14685
+ x1: external_exports.number(),
14686
+ y1: external_exports.number(),
14687
+ x2: external_exports.number(),
14688
+ y2: external_exports.number()
14689
+ }),
14690
+ external_exports.object({
14691
+ ...annotationCommon,
14692
+ kind: external_exports.literal("pencil"),
14693
+ points: external_exports.array(AnnotationPointSchema).min(2)
14694
+ }),
14695
+ external_exports.object({
14696
+ ...annotationCommon,
14697
+ kind: external_exports.literal("highlighter"),
14698
+ points: external_exports.array(AnnotationPointSchema).min(2)
14699
+ }),
14700
+ external_exports.object({
14701
+ ...annotationCommon,
14702
+ kind: external_exports.literal("text"),
14703
+ x: external_exports.number(),
14704
+ y: external_exports.number(),
14705
+ text: external_exports.string(),
14706
+ fontSize: external_exports.number().optional()
14707
+ }),
14708
+ external_exports.object({
14709
+ ...annotationCommon,
14710
+ kind: external_exports.literal("step"),
14711
+ x: external_exports.number(),
14712
+ y: external_exports.number(),
14713
+ n: external_exports.number()
14714
+ })
14715
+ ]);
14645
14716
  var AssetKindSchema = external_exports.enum(["replay", "console", "network", "dom", "screenshot", "attachment"]);
14646
14717
  var AssetRefSchema = external_exports.object({
14647
14718
  kind: AssetKindSchema,
@@ -29492,7 +29563,20 @@ var ReplayRecorder = class {
29492
29563
  console.warn("[lens] rrweb recording unavailable; replay disabled", err);
29493
29564
  }
29494
29565
  };
29495
- async function captureScreenshot(excludeHost, crop) {
29566
+ var SVG_RASTERIZE_TIMEOUT_MS = 3e3;
29567
+ function computeCropRect(crop, scale, rasterWidth, rasterHeight) {
29568
+ let sx = Math.round(crop.x * scale);
29569
+ let sy = Math.round(crop.y * scale);
29570
+ let sw = Math.round(crop.width * scale);
29571
+ let sh = Math.round(crop.height * scale);
29572
+ sx = Math.max(0, Math.min(sx, rasterWidth));
29573
+ sy = Math.max(0, Math.min(sy, rasterHeight));
29574
+ sw = Math.min(sw, rasterWidth - sx);
29575
+ sh = Math.min(sh, rasterHeight - sy);
29576
+ if (sw <= 0 || sh <= 0) return null;
29577
+ return { sx, sy, sw, sh };
29578
+ }
29579
+ async function captureScreenshot(excludeHost, crop, composite) {
29496
29580
  if (typeof document === "undefined" || typeof window === "undefined") return null;
29497
29581
  try {
29498
29582
  const viewportPixels = window.innerWidth * window.innerHeight || 1;
@@ -29502,6 +29586,15 @@ async function captureScreenshot(excludeHost, crop) {
29502
29586
  scale,
29503
29587
  type: "image/jpeg",
29504
29588
  quality: 0.82,
29589
+ // JPEG has no alpha — transparent page regions (and any capture box
29590
+ // extending past short content) would otherwise encode as BLACK.
29591
+ backgroundColor: "#ffffff",
29592
+ // When compositing annotations, force the capture box to the editor's
29593
+ // document measurement (scrollHeight-based). modern-screenshot otherwise
29594
+ // sizes from documentElement's bounding box, which on pages SHORTER than
29595
+ // the viewport is the content height only — marks drawn in the lower
29596
+ // viewport band would fall off the raster and the crop would clamp short.
29597
+ ...composite ? { width: composite.docWidth, height: composite.docHeight } : {},
29505
29598
  // Exclude the overlay from the capture CLONE — never hide the live node.
29506
29599
  filter: (node2) => node2 !== excludeHost,
29507
29600
  // Per-resource fetch budget: a single slow cross-origin image must not
@@ -29526,6 +29619,16 @@ async function captureScreenshot(excludeHost, crop) {
29526
29619
  );
29527
29620
  return null;
29528
29621
  }
29622
+ if (composite) {
29623
+ const baked = await compositeScreenshot(blob2, composite, scale, crop);
29624
+ if (baked) return baked;
29625
+ if (composite.blurRegions.length > 0) {
29626
+ console.warn(
29627
+ "[lens] screenshot compositing failed with active redactions; dropping screenshot to avoid leaking un-blurred content"
29628
+ );
29629
+ return null;
29630
+ }
29631
+ }
29529
29632
  if (crop) {
29530
29633
  const cropped = await cropBlob(blob2, crop, scale);
29531
29634
  return cropped ?? blob2;
@@ -29536,35 +29639,146 @@ async function captureScreenshot(excludeHost, crop) {
29536
29639
  return null;
29537
29640
  }
29538
29641
  }
29642
+ async function compositeScreenshot(blob2, composite, scale, crop) {
29643
+ try {
29644
+ if (typeof createImageBitmap !== "function" || typeof document === "undefined") return null;
29645
+ const bitmap = await createImageBitmap(blob2);
29646
+ const canvas = document.createElement("canvas");
29647
+ canvas.width = bitmap.width;
29648
+ canvas.height = bitmap.height;
29649
+ const ctx = canvas.getContext("2d");
29650
+ if (!ctx) {
29651
+ bitmap.close?.();
29652
+ return null;
29653
+ }
29654
+ ctx.fillStyle = "#ffffff";
29655
+ ctx.fillRect(0, 0, canvas.width, canvas.height);
29656
+ ctx.drawImage(bitmap, 0, 0);
29657
+ bitmap.close?.();
29658
+ applyBlurRegions(ctx, canvas, composite.blurRegions, scale);
29659
+ await drawSvgOverlay(ctx, composite, scale);
29660
+ if (crop) {
29661
+ const rect = computeCropRect(crop, scale, canvas.width, canvas.height);
29662
+ if (rect) {
29663
+ const out = document.createElement("canvas");
29664
+ out.width = rect.sw;
29665
+ out.height = rect.sh;
29666
+ const octx = out.getContext("2d");
29667
+ if (octx) {
29668
+ octx.drawImage(canvas, rect.sx, rect.sy, rect.sw, rect.sh, 0, 0, rect.sw, rect.sh);
29669
+ return await canvasToJpegBlob(out);
29670
+ }
29671
+ }
29672
+ }
29673
+ return await canvasToJpegBlob(canvas);
29674
+ } catch (err) {
29675
+ console.warn("[lens] screenshot compositing failed:", err);
29676
+ return null;
29677
+ }
29678
+ }
29679
+ function applyBlurRegions(ctx, canvas, regions, scale) {
29680
+ if (regions.length === 0) return;
29681
+ const filterSupported = typeof ctx.filter === "string";
29682
+ const blurPx = Math.max(8, 12 * scale);
29683
+ const pad = Math.ceil(blurPx);
29684
+ for (const region of regions) {
29685
+ const x = Math.max(0, Math.min(Math.round(region.x * scale), canvas.width));
29686
+ const y = Math.max(0, Math.min(Math.round(region.y * scale), canvas.height));
29687
+ const w = Math.min(Math.round(region.width * scale), canvas.width - x);
29688
+ const h = Math.min(Math.round(region.height * scale), canvas.height - y);
29689
+ if (w <= 0 || h <= 0) continue;
29690
+ if (!filterSupported) {
29691
+ ctx.fillStyle = "rgba(127,127,127,1)";
29692
+ ctx.fillRect(x, y, w, h);
29693
+ continue;
29694
+ }
29695
+ const px = Math.max(0, x - pad);
29696
+ const py = Math.max(0, y - pad);
29697
+ const pw = Math.min(canvas.width, x + w + pad) - px;
29698
+ const ph = Math.min(canvas.height, y + h + pad) - py;
29699
+ if (pw <= 0 || ph <= 0) continue;
29700
+ const temp = document.createElement("canvas");
29701
+ temp.width = pw;
29702
+ temp.height = ph;
29703
+ const tctx = temp.getContext("2d");
29704
+ if (!tctx) {
29705
+ ctx.fillStyle = "rgba(127,127,127,1)";
29706
+ ctx.fillRect(x, y, w, h);
29707
+ continue;
29708
+ }
29709
+ tctx.filter = `blur(${blurPx}px)`;
29710
+ tctx.drawImage(canvas, px, py, pw, ph, 0, 0, pw, ph);
29711
+ tctx.filter = "none";
29712
+ ctx.save();
29713
+ ctx.beginPath();
29714
+ ctx.rect(x, y, w, h);
29715
+ ctx.clip();
29716
+ ctx.drawImage(temp, px, py);
29717
+ ctx.restore();
29718
+ }
29719
+ }
29720
+ async function drawSvgOverlay(ctx, composite, scale) {
29721
+ const { svgMarkup, docWidth, docHeight } = composite;
29722
+ if (!svgMarkup) return;
29723
+ const img = await loadSvgImage(svgMarkup);
29724
+ if (!img) return;
29725
+ try {
29726
+ ctx.drawImage(img, 0, 0, docWidth * scale, docHeight * scale);
29727
+ } catch (err) {
29728
+ console.warn("[lens] annotation overlay draw failed:", err);
29729
+ }
29730
+ }
29731
+ function loadSvgImage(svg) {
29732
+ return new Promise((resolve) => {
29733
+ if (typeof Image !== "function") {
29734
+ resolve(null);
29735
+ return;
29736
+ }
29737
+ let settled = false;
29738
+ const finish = (result2) => {
29739
+ if (settled) return;
29740
+ settled = true;
29741
+ clearTimeout(timer);
29742
+ resolve(result2);
29743
+ };
29744
+ const timer = setTimeout(() => {
29745
+ console.warn("[lens] annotation overlay rasterization timed out");
29746
+ finish(null);
29747
+ }, SVG_RASTERIZE_TIMEOUT_MS);
29748
+ const img = new Image();
29749
+ img.onload = () => finish(img);
29750
+ img.onerror = () => {
29751
+ console.warn("[lens] annotation overlay failed to rasterize");
29752
+ finish(null);
29753
+ };
29754
+ img.src = "data:image/svg+xml;charset=utf-8," + encodeURIComponent(svg);
29755
+ });
29756
+ }
29757
+ function canvasToJpegBlob(canvas) {
29758
+ return new Promise((resolve) => {
29759
+ canvas.toBlob((out) => resolve(out), "image/jpeg", 0.82);
29760
+ });
29761
+ }
29539
29762
  async function cropBlob(blob2, crop, scale) {
29540
29763
  try {
29541
29764
  if (typeof createImageBitmap !== "function" || typeof document === "undefined") return null;
29542
29765
  const bitmap = await createImageBitmap(blob2);
29543
- let sx = Math.round(crop.x * scale);
29544
- let sy = Math.round(crop.y * scale);
29545
- let sw = Math.round(crop.width * scale);
29546
- let sh = Math.round(crop.height * scale);
29547
- sx = Math.max(0, Math.min(sx, bitmap.width));
29548
- sy = Math.max(0, Math.min(sy, bitmap.height));
29549
- sw = Math.min(sw, bitmap.width - sx);
29550
- sh = Math.min(sh, bitmap.height - sy);
29551
- if (sw <= 0 || sh <= 0) {
29766
+ const rect = computeCropRect(crop, scale, bitmap.width, bitmap.height);
29767
+ if (!rect) {
29552
29768
  bitmap.close?.();
29553
29769
  return null;
29554
29770
  }
29555
29771
  const canvas = document.createElement("canvas");
29556
- canvas.width = sw;
29557
- canvas.height = sh;
29772
+ canvas.width = rect.sw;
29773
+ canvas.height = rect.sh;
29558
29774
  const ctx = canvas.getContext("2d");
29559
29775
  if (!ctx) {
29560
29776
  bitmap.close?.();
29561
29777
  return null;
29562
29778
  }
29563
- ctx.drawImage(bitmap, sx, sy, sw, sh, 0, 0, sw, sh);
29779
+ ctx.drawImage(bitmap, rect.sx, rect.sy, rect.sw, rect.sh, 0, 0, rect.sw, rect.sh);
29564
29780
  bitmap.close?.();
29565
- return await new Promise((resolve) => {
29566
- canvas.toBlob((out) => resolve(out), "image/jpeg", 0.82);
29567
- });
29781
+ return await canvasToJpegBlob(canvas);
29568
29782
  } catch (err) {
29569
29783
  console.warn("[lens] screenshot crop failed:", err);
29570
29784
  return null;
@@ -29834,7 +30048,11 @@ var LensClient = class {
29834
30048
  const wantErrors = include.errors !== false;
29835
30049
  const wantScreenshot = include.screenshot !== false;
29836
30050
  const wantDom = include.dom !== false;
29837
- const screenshotPromise = this.screenshotEnabled && wantScreenshot ? captureScreenshot(input2.excludeFromScreenshot, input2.screenshotCrop) : Promise.resolve(null);
30051
+ const screenshotPromise = this.screenshotEnabled && wantScreenshot ? captureScreenshot(
30052
+ input2.excludeFromScreenshot,
30053
+ input2.screenshotCrop,
30054
+ input2.annotationComposite
30055
+ ) : Promise.resolve(null);
29838
30056
  const replayEvents = wantReplay ? this.replay?.snapshot() ?? [] : [];
29839
30057
  const errorEntries = this.errorBuffer.drain();
29840
30058
  const consoleTail = this.consoleBuffer.tail(CONSOLE_TAIL_SIZE);