npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

glove-env-render

v1.1.0

Published

Rendering stdlib adapter for glove-working-environment. Rasterizes PDFs, slide decks, Word files and images to page PNGs inside the agent's virtual filesystem as env:render — so an agent can look at what it produced instead of only reading its text back.

Readme

glove-env-render

Pictures of the agent's own output, so it can check its work by looking at it.

Every other way of verifying in glove-working-environment reads text back. That finds a wrong number and misses a table running off the page, a chart with no bars, a title overlapping its subtitle, or a final slide that came out blank — the defects a person notices in the first second.

pnpm add glove-env-render

Two halves

This package rasterizes to PNG inside the VFS:

import { createWorkingEnvironment } from "glove-working-environment";
import { render } from "glove-env-render";

const env = await createWorkingEnvironment({ stdlib: [render()] });
// …and a script can then do:
import { render } from 'env:render';
const shot = await render('/out/report.pdf', '/tmp/proof');   // → /tmp/proof/report-p1.png

The view_image verb (in glove-working-environment, enabled when the host wires a vision model) is what actually looks at it — and it renders for you, so verification is one call:

const env = await createWorkingEnvironment({
  stdlib: [render()],
  vision: {
    async describe({ bytes, mediaType, prompt }) {
      // your provider — this is the whole contract
      return await myVisionModel(bytes, mediaType, prompt);
    },
  },
});
view_image({
  path: '/out/report.pdf',
  prompt: 'This should list four regions with a total. Name every region and
           figure you can see, and say whether any text is cut off or overlapping.'
})

// A later page or slide — still no rendering step:
view_image({ path: '/out/deck.pptx', page: 3, prompt: 'Is this slide blank?' })

Measured against a report with two deliberate defects — a row pushed off the right edge and a subtitle overlapping the title — a commodity vision model reported both, unprompted about either specifically.

What it renders

| Input | Needs | |---|---| | .pdf | nothing | | .png .jpg .webp .gif .bmp .tiff .avif | nothing | | .pptx | nothing — falls back to a layout schematic | | .docx .xlsx .odp .odt .ods | headless LibreOffice on the host | | .doc .xls .ppt .rtf | the same LibreOffice — the legacy formats an inbox is full of |

PDFs go through pdfjs-dist onto @napi-rs/canvas — both are ordinary dependencies with prebuilt binaries, no system packages.

The legacy formats are here because attachments are where they still turn up. Nothing in the environment parses a .doc or a .rtf, so rasterizing it and reading the page — with env:ocr, or a vision model through view_image — is the whole route from those bytes to text. describe says so on a file nothing else claims.

Office formats go through LibreOffice, because no npm package renders .pptx faithfully. libreoffice-core alone is not enough: it ships no import filters, and every conversion fails with source file could not be loaded while exiting 0. Install libreoffice-impress for decks and libreoffice-writer for Word. The adapter detects exactly that case and says so.

If LibreOffice is not available, generate the PDF directly instead — glove-env-documents renders the same document spec to PDF with no system dependency.

Decks without LibreOffice: the layout schematic

A .pptx is the one Office format that still gets a visual check with nothing installed. Rather than fail, it is drawn from its own OOXML geometry — every shape's real frame and real text, to scale.

It is not a render and never pretends to be. No theme colours, no fonts from the deck, no charts, no SmartArt, no master-slide inheritance. The result carries approximate: true, and the image itself ends in a caption saying what it is.

What it does catch is everything positional, which is most of what goes wrong:

  • a text box hanging off the slide (drawn outside the white area, outlined red)
  • text that overflows its own box (outlined red, the overflow drawn anyway)
  • a slide with nothing on it
  • shapes stacked on top of each other

Two details exist because a vision model got them wrong first:

  • An empty slide is left genuinely empty. Drawing "(this slide is empty)" onto it made a model answer "yes, this slide has content" — it read the notice as content. The fact went to the caption instead.
  • The caption says "thin rectangles are shape frames, not visible borders." Without that line a model read a frame as a clipping boundary and reported a title cut off that was not.

With both, asked "list the text and say whether anything sits outside the slide", a commodity vision model named the on-slide text correctly and flagged only the genuinely off-slide box.

Pass schematicFallback: false to get the LibreOffice error instead.

At scale, don't spawn LibreOffice

Spawning soffice per file costs ~1s of process start, and no amount of tuning removes it. Every platform that renders Office documents in volume keeps LibreOffice warm behind a queue instead — Gotenberg, unoserver, JODConverter and Collabora are all that shape. The ones that avoid LibreOffice entirely pay for a native SDK.

So the escape hatch is a function, and the ceiling becomes theirs rather than ours:

render({
  async convertOffice(bytes, filename) {
    const form = new FormData();
    form.set("files", new Blob([bytes]), filename);
    const res = await fetch("http://gotenberg:3000/forms/libreoffice/convert", {
      method: "POST",
      body: form,
    });
    if (!res.ok) throw new Error(`gotenberg ${res.status}`);
    return new Uint8Array(await res.arrayBuffer());
  },
})

With it set, soffice is never invoked and no profile pool is created. PDFs and images are unaffected — they never needed LibreOffice.

Without it, conversions lease from a small pool of reused LibreOffice profiles. Concurrent conversions cannot share a profile, but a fresh one pays first-run initialization, so profiles are leased exclusively and returned. Measured at 8 conversions per arm, interleaved, comparing medians:

| strategy | median | spread | |---|---|---| | a fresh profile each | 1297ms | 1089–2016 | | a leased profile reused | 1019ms | 986–1183 |

About 21%, and the tighter spread matters as much as the median. profilePoolSize (default 2) is how many conversions can run at once.

API

render(input: string, outDir: string, opts?: {
  pages?: number[] | "all";   // 1-based. Default [1]
  scale?: number;             // Default 1.5
  maxWidth?: number;          // Long-edge cap. Default 1600
}): Promise<{
  pages: Array<{ path: string; page: number; width: number; height: number; bytes: number }>;
  format: "pdf" | "office" | "image";
  totalPages: number;         // the document's length, not the render's
  approximate?: true;         // set only for a .pptx layout schematic
  note?: string;              // why, when something was degraded
}>

Host-side options: render({ sofficePath, officeTimeoutMs, maxWidth, maxPages, profilePoolSize, convertOffice, schematicFallback }).

Three details that are deliberate:

  • Format is decided by magic bytes, then extension. A PDF named .pptx is a PDF and never reaches LibreOffice.
  • maxWidth caps the long edge at 1600px. A vision model charges by pixels and reads an A4 page perfectly well at that size; rendering at scale 3 costs several times more and answers no better.
  • Each LibreOffice conversion gets a private user profile. LibreOffice locks its user-installation directory and opens an IPC socket named after it — the machinery that makes a second document open in the LibreOffice you already have running. Headless, a second conversion sharing that profile tries to delegate to the running instance instead of converting, and exits without writing anything. Measured: four concurrent conversions on a shared profile produced two PDFs; the same four with a profile each produced four. The losers are inconsistent — some exit 1, some exit 0 having done nothing — so success is decided by whether a PDF appeared, never by the exit code.

Verifying it yourself

pnpm test covers the PDF and image paths for real: it generates a PDF, rasterizes it, and asserts on the bytes. The LibreOffice test skips with a message when the import filters are absent, rather than quietly passing.

tests/live-check.mts is a manual end-to-end check against a real vision model — it builds a report with known defects, renders it, and prints what the model saw. Needs OPENROUTER_API_KEY; it is not part of CI.

pnpm exec tsx tests/live-check.mts

Password-protected inputs

This adapter exports unlock(input, output, { password }) for PDF, encrypted DOCX/XLSX/PPTX and ZIP (AES/ZipCrypto). Call it with the known password and a new output path, then use the returned path with the usual reader, editor, renderer or OCR operation. The original remains unchanged; the copy is unencrypted and subject to normal VFS limits and persistence.

See glove-env-unlock for examples, supported formats, and the host-side API for keeping passwords out of script history.