Skip to content

From images to a searchable Manifest

You have a set of page images — a book, a pamphlet, an archive folder. This recipe takes them and produces a IIIF Manifest with search inside: a viewer can search the text, see hits highlighted on the page, and get autocomplete suggestions. Along the way the platform also builds a searchable PDF of the whole work.

The recipe uses eight pages of a public-domain pamphlet from Wellcome Collection (The energy index, b3343136x). The runnable scripts and fixture images are in this documentation’s repository:

Four platform features combine. There are two API calls that are significant:

  1. Assets — each page image is registered once, via a Queue batch.
  2. Adjuncts — each asset carries its OCR text as an ALTO XML adjunct. The platform fetches, stores and serves the ALTO alongside the image.
  3. A stored Manifest — one PUT creates the Manifest from the assets and attaches the adjuncts and requests the pipeline.
  4. The text pipeline — indexes the ALTO and augments the public Manifest with a IIIF Content Search 2 service.

The text itself is produced by you, outside the platform — that’s the pluggable middle of this recipe. Any process that emits ALTO XML works: Tesseract (used here), a commercial OCR service, an HTR model, or a digitisation vendor’s existing METS-ALTO.

Registration is ordinary queue-based batch registration. Two details are important for later: number1 records the page sequence, and string1 tags every page with the work it belongs to, so the whole book is easy to query as a unit.

{
"@type": "Collection",
"member": [
{
"id": "energy-index_0001",
"space": 98765,
"origin": "https://example.org/scans/energy-index/0001.jpg",
"mediaType": "image/jpeg",
"string1": "energy-index",
"number1": 1
},
...
]
}

Step 2: generate the text — the pluggable step

Section titled “Step 2: generate the text — the pluggable step”

This recipe uses Tesseract because it is free and runs anywhere, but the platform doesn’t care where your ALTO comes from. The recipe isolates the OCR in one small function so you can swap in anything:

def generate_alto(image_path):
"""Produce ALTO XML for one page image.
THE PLUGGABLE STEP: replace the body of this function with your own
OCR/HTR engine, cloud service, or a lookup into text you already have.
It just has to return ALTO XML for the page.
"""

The output needs to end up at an HTTP(S) location the platform can fetch from — the ALTO files become adjunct origins in the next step, exactly like asset origins (origin strategies apply if they need credentials). This recipe’s ALTO is committed to the documentation site itself, so the scripts run without you staging anything.

This is the heart of the recipe. A single PUT to the Manifest’s URL:

  • creates the Manifest, generating a canvas per asset in canvasOrder;
  • registers an ALTO adjunct against each asset, fetched from its origin;
  • expresses each adjunct as a seeAlso link on its canvas;
  • queues the text pipeline.
{
"type": "Manifest",
"label": { "en": ["The energy index"] },
"slug": "energy-index",
"parent": "https://iiif.dlcs.example/99/collections/root",
"paintedResources": [
{
"asset": {
"id": "energy-index_0001",
"space": 98765,
"adjuncts": [
{
"id": "alto.xml",
"origin": "https://example.org/text/energy-index/0001.xml",
"@type": "Dataset",
"mediaType": "text/xml",
"profile": "http://www.loc.gov/standards/alto/v3/alto.xsd",
"label": { "en": ["OCR text for page 1"] },
"iiifLink": "seeAlso"
}
]
},
"canvasPainting": { "canvasOrder": 0, "label": { "en": ["Page 1"] } }
},
...
],
"pipeline": [
{ "name": "text", "config": { "action": "Index" } }
]
}

The response is 202 Accepted, and the Manifest’s API view reports both kinds of background work: ingesting while the adjuncts are fetched, then the pipeline lifecycle until indexing finishes. The public URL returns 404 until then, so nobody sees a half-enriched Manifest.

"finishedPipelines": [
{
"name": "text",
"config": { "action": "Index" },
"status": "Completed",
"created": "2026-09-14T08:24:54.461675Z",
"finished": "2026-09-14T08:24:57.387773Z"
}
]

The public Manifest now carries the OCR on every canvas as seeAlso:

"seeAlso": [
{
"id": "https://dlcs.example/adjuncts/99/98765/energy-index_0001/alto.xml",
"type": "Dataset",
"profile": "http://www.loc.gov/standards/alto/v3/alto.xsd",
"label": { "en": ["OCR text for page 1"] },
"format": "text/xml"
}
]

and a IIIF Content Search 2 service at Manifest level, with autocomplete nested inside — any viewer that supports Content Search gets search-within-this-work with no further effort from you:

"service": [
{
"id": "https://dlcs.example/search/v2/99/iiif/energy-index",
"type": "SearchService2",
"profile": "http://iiif.io/api/search/2/search",
"label": { "en": ["Search within this manifest"] },
"service": [
{
"id": "https://dlcs.example/autocomplete/v2/99/iiif/energy-index",
"type": "AutoCompleteService2",
"profile": "http://iiif.io/api/search/2/autocomplete",
"label": { "en": ["Autocomplete words in this manifest"] }
}
]
}
]

Hits come back as annotations targeting the canvas with word coordinates, so viewers can paint highlight boxes on the page image:

{
"id": "https://dlcs.example/search/v2/99/iiif/energy-index/anno/h0i0-839,600,511,89",
"type": "Annotation",
"motivation": "painting",
"body": { "type": "TextualBody", "value": "ENERGY", "format": "text/plain" },
"target": "https://iiif.dlcs.example/99/canvases/6t0acvs32uhcvd3bval10e#xywh=839,600,511,89"
}

with accompanying contextualizing annotations giving each hit’s surrounding text for snippet display.

Indexing is not the only thing the text pipeline does with your ALTO: it also builds a PDF of the whole Manifest with an embedded text layer — the page images with your OCR behind them, so the PDF itself is selectable and searchable. It is linked from the public Manifest as a rendering, ready for a “Download PDF” button in your viewer or site.

What if the manifest already exists? In this scenario you use the platform to host image assets and generate image services, but you made the manifest that references them independently. You still want to use these text services to enrich the manifest.

You can save your existing external Manifest so that it becomes a managed Manifest (see IIIF Presentation Support). Save your manifest to the platform as-is (an ordinary IIIF document), then make one update that:

  • keeps every canvas, reduced to { "id": ..., "type": "Canvas" } placeholders — your canvas ids are preserved;
  • adds paintedResources connecting each canvas id to its platform asset (with canvasPainting.canvasId set to your authored id);
  • requests the text pipeline.

The platform recognises its own assets: nothing is reingested, your canvas ids survive on the public Manifest, and the assets’ ALTO adjuncts appear on your canvases as seeAlso.

The recipe composes from independent pieces, so skip what you’ve already done:

  • Images already registered? Start at step 2 — you just need each asset’s id and space for the manifest call.
  • Text already exists? Digitisation workflows often already produced METS-ALTO. Skip OCR entirely: point the adjunct origins at your existing ALTO (per-page files).
  • Handwritten material? Swap generate_alto() for an HTR engine — anything that emits ALTO plugs in.
  • Manifest already published? Use the adoption flow above.

OCR improves; you re-run it and want the new text live. Adjuncts are ordinary resources — you can update each asset’s alto.xml adjunct directly. But note that the Manifest is enriched when it is saved: after changing adjuncts outside a manifest operation, re-PUT the Manifest (same body, including the pipeline) to re-express the links and re-index the new text.

The inline asset.adjuncts array in a manifest PUT is a synchronisation: it makes the asset’s adjuncts match the array, so the same step-3 call with updated origins is also the update call. (Corollary: an empty "adjuncts": [] deletes the asset’s adjuncts — omit the property entirely when you don’t want to touch them.)