Curiosity

Render file pages as images

When you need a file's pages as images — to send to a multimodal LLM, generate a per-page preview, or store as a side-by-side asset next to the original — call ChatAI.RenderFilePageAsImageAsync. The workspace's built-in extractor renders each page as a JPEG, stores it as a blob, and returns a map of page index to blob UID.

For an overview of what the extractor does and when, see OCR & File Extraction.

The two signatures

RenderFilePageAsImageAsync is available wherever the safe-graph ChatAI helper is — Custom Endpoints, Code Indexes, scheduled tasks, search scopes:

// Single page (0-based)
Dictionary<int, UID128> pageBlobs = await ChatAI.RenderFilePageAsImageAsync(fileUID, pageIndex: 0, CancellationToken);

// Inclusive range of pages (0-based, both inclusive)
Dictionary<int, UID128> pageBlobs = await ChatAI.RenderFilePageAsImageAsync(fileUID, startPageIndex: 0, endPageIndex: 4, CancellationToken);

Both signatures return the same shape: a dictionary keyed by 0-based page index, with each value the UID of the JPEG blob holding that page. Pages the file does not have, or that the extractor failed to render, are simply omitted from the result.

A few things to know:

  • The call blocks on extraction, so the first invocation for a large PDF can take a few seconds per page. The cancellation token is honoured at every stage.
  • Page indexes are always 0-based. Page 0 is the first page.
  • The blob UID for page N of file F is derived deterministically from the identifier "{F}-RENDER-PAGE-{N}". Re-calling for the same page returns the same blob UID, so the call is idempotent — no need to cache the result yourself.
  • Renders are JPEGs at ~1568 px on the long side, sized for multimodal LLM consumption.
  • Supported file types are the same set that produce page thumbnails: PDF, Word, PowerPoint, Excel, images (single-page), PSD.

Because the blob UID is deterministic, you can call RenderFilePageAsImageAsync as many times as you like — the second call for the same page returns the same blob UID and skips re-rendering nothing if the blob already exists is up to the caller. The simplest pattern is to call it on demand and trust the cache:

file/page-as-image
// POST /api/file/page-as-image?fileUID=…&page=2
var fileUID    = UID128.Parse(Query["fileUID"]);
var pageIndex  = int.Parse(Query["page"]);

var pageBlobs  = await ChatAI.RenderFilePageAsImageAsync(fileUID, pageIndex, CancellationToken);

if (pageBlobs.TryGetValue(pageIndex, out var blobUID))
{
    // Return the blob UID; the front-end can fetch /api/files/blob/{uid} to download the JPEG.
    return Ok(blobUID);
}
return NotFound();

Sending a page to a multimodal model

The blob UIDs returned from RenderFilePageAsImageAsync are first-class attachments — they can be passed straight to ChatAI.AddAssistantMessageAsync or ChatAI.AddUserMessageAsync via the attachmentUIDs parameter, and the assistant will see them as images:

var pages = await ChatAI.RenderFilePageAsImageAsync(fileUID, 0, 4, CancellationToken);

await ChatAI.AddUserMessageAsync(
    chatUID:        chatUID,
    userUID:        userUID,
    text:           "Describe what's on the first five pages of this document.",
    attachmentUIDs: pages.Values.ToArray());

In a Code Index attached to the File type

When you want every uploaded file to have rendered pages cached on its node, do the work in a Code Index. The deterministic blob UID makes the second pass a no-op:

Code Index — Page renders
foreach (var fileUid in ToIndex)
{
    if (CancellationToken.IsCancellationRequested) return ToIndex;

    // Render the first 10 pages. The blobs are deterministic, so re-runs are cheap.
    var pages = await ChatAI.RenderFilePageAsImageAsync(fileUid, 0, 9, CancellationToken);

    var fileNode = await Graph.GetOrAddLockedAsync(N._FileEntry.Type, fileUid.ToString());

    foreach (var (pageIndex, blobUID) in pages)
    {
        // Attach each page render under a per-page edge for downstream lookup.
        fileNode.AddUniqueEdge(E.HasPageRender, blobUID, N._Blob.Type);
    }

    await Graph.CommitAsync(fileNode);
}

return new List<UID128>();
© 2026 Curiosity. All rights reserved.