survos/iiif-bundle

PHP 8.4+ Symfony bundle for generating IIIF Presentation API 3.0 manifests

Maintainers

πŸ‘ tacman1123

Package info

github.com/survos/iiif-bundle

Type:symfony-bundle

pkg:composer/survos/iiif-bundle

Fund package maintenance!

kbond

Statistics

Installs: 501

Dependents: 2

Suggesters: 0

Stars: 0

Open Issues: 0

2.10.27 2026-06-25 16:40 UTC

Suggests

Provides

None

Conflicts

None

Replaces

None

MIT ab4bb092bd8acae0f7c7f094138481d47d1f71f4

symfonyuxVieweropenseadragoniiifsymfony-uxdiva

This package is auto-updated.

Last update: 2026-06-25 16:41:50 UTC


README

PHP 8.4+ Symfony bundle for generating IIIF Presentation API 3.0 manifests.

@todo: make sure it works with https://manuscrits-france-angleterre.org/view3if/?target=https://gallica.bnf.fr/iiif/ark:/12148/bpt6k9907264/manifest.json&page=2

Installation

composer require survos/iiif-bundle

Quick Start

use Survos\IiifBundle\Builder\ManifestBuilder;
use Survos\IiifBundle\Model\ImageService3;
use Survos\IiifBundle\Enum\ViewingDirection;
use Survos\IiifBundle\Enum\Behavior;

$builder = new ManifestBuilder('https://example.org/iiif/item-123/manifest');

$builder
 ->setLabel('en', 'Civil War Pension File β€” Pvt. James Wilson')
 ->setSummary('en', 'Pension application including affidavits and medical examination, 1892')
 ->addMetadata('en', 'Date', 'en', '1892')
 ->addMetadata('en', 'Creator', 'en', 'U.S. Pension Bureau')
 ->setRights('http://creativecommons.org/publicdomain/mark/1.0/')
 ->setRequiredStatement('en', 'Attribution', 'Courtesy of Carver 4-County Museum')
 ->setViewingDirection(ViewingDirection::LEFT_TO_RIGHT)
 ->setBehavior(Behavior::PAGED);

// Add pages as canvases
foreach ($pages as $i => $page) {
 $canvas = $builder->addCanvas(
 id: "https://example.org/iiif/item-123/canvas/p{$i}",
 label: "Page {$i}",
 width: $page->getWidth(),
 height: $page->getHeight(),
 );

 // Paint the image onto the canvas
 $canvas->addImage(
 annotationId: "https://example.org/iiif/item-123/canvas/p{$i}/anno/image",
 imageUrl: "https://s3.example.org/scans/item-123/page-{$i}.jpg",
 format: 'image/jpeg',
 width: $page->getWidth(),
 height: $page->getHeight(),
 service: new ImageService3(
 id: "https://iiif.example.org/image/item-123-page-{$i}",
 profile: 'level2'
 ),
 );

 // Add full-page OCR text as supplementing annotation
 $canvas->addSupplementingText(
 annotationId: "https://example.org/iiif/item-123/canvas/p{$i}/ocr/fulltext",
 text: $page->getOcrText(),
 language: 'en',
 );

 // Add word-level OCR annotations for search highlighting
 foreach ($page->getOcrWords() as $word) {
 $canvas->addWordAnnotation(
 annotationId: "https://example.org/iiif/item-123/canvas/p{$i}/ocr/word-" . $word->getId(),
 text: $word->getText(),
 language: 'en',
 x: $word->getX(),
 y: $word->getY(),
 width: $word->getWidth(),
 height: $word->getHeight(),
 );
 }
}

// Add Content Search service (Meilisearch-backed)
$builder->addSearchService('https://example.org/iiif/item-123/search');

// Serialize to JSON
$json = $builder->toJson();
$array = $builder->toArray();

Source images vs. IIIF endpoints β€” the IiifUrl resolver

ItemField::IIIF_BASE (iiifBase) is overloaded. For most providers it is just the largest non-tiff source image β€” a plain, fetchable URL that imgproxy resizes (Fortepan, Smithsonian IDS, Walters, Cleveland…). The shared NormalizeFallbackListener (data-bundle) even sets iiifBase = largeImageUrl for every row. Only genuine IIIF sources (e.g. Digital Commonwealth) store an actual IIIF Image API base.

Survos\IiifBundle\Service\IiifUrl is the single place that distinguishes them and turns either into a fetchable image URL:

IiifUrl::isImageApiEndpoint($url); // true only for real IIIF (contains /iiif/ or ends /info.json)
IiifUrl::imageUrl($base); // real IIIF β†’ "$base/full/max/0/default.jpg"
 // direct image β†’ "$base" unchanged
IiifUrl::imageUrl($base, '!300,300'); // custom IIIF size segment

Never decide this by file extension. The old heuristic ("no extension β‡’ it's a IIIF base, append /full/max/0/default.jpg") breaks both ways:

  • extensionless direct images β€” Smithsonian IDS …/deliveryService?id=X got …?id=X/full/max/0/default.jpg (404 / "Invalid URL");
  • already-an-image URLs β€” Fortepan …/fortepan_266.jpg got …/fortepan_266.jpg/full/max/0/default.jpg (dead link).

Resolve once, at normalize β€” not at sync/render

Image-URL resolution should happen once, during normalization, yielding a concrete fetchable imageUrl. Everything downstream consumes that known URL:

  • media:sync historically expected raw URLs only β€” it ships known URLs to mediary and must not derive iiifBase + /full/max/… itself.
  • Read models / templates should prefer the pre-resolved imageUrl.

Append sites β€” audit (June 2026)

Each of these re-derives the image URL instead of consuming a resolved one, guessing IIIF-ness independently. They should call IiifUrl (better: consume the normalize-time imageUrl):

  • folio-bundle Row::getThumbnailSource, FolioChatHit::thumbnailUrl, FolioAiPromptBuilder β€” migrated to IiifUrl.
  • media-bundle MediaSyncItem (url/preferredUrl from iiifBase), MediaShow::fullSizeUrl β€” still derive; media:sync should take raw URLs only.
  • meili-bundle search-card template (TemplateController) β€” still appends iiifBase ~ '/full/' ~ size.
  • iiif-bundle ManifestSummaryExtractor, IiifExtension, IiifSize β€” these build from a genuine IIIF base (parsed manifest / explicit caller), so appending is correct.

Two NormalizeFallbackListeners set iiifBase today (import-bundle, guarded on /iiif/; data-bundle, unconditional from largeImageUrl) β€” these should converge into one source-image resolution that also emits the concrete imageUrl.

Output Example

{
 "@context": "http://iiif.io/api/presentation/3/context.json",
 "id": "https://example.org/iiif/item-123/manifest",
 "type": "Manifest",
 "label": { "en": ["Civil War Pension File β€” Pvt. James Wilson"] },
 "summary": { "en": ["Pension application including affidavits and medical examination, 1892"] },
 "metadata": [
 { "label": { "en": ["Date"] }, "value": { "en": ["1892"] } },
 { "label": { "en": ["Creator"] }, "value": { "en": ["U.S. Pension Bureau"] } }
 ],
 "rights": "http://creativecommons.org/publicdomain/mark/1.0/",
 "requiredStatement": {
 "label": { "en": ["Attribution"] },
 "value": { "en": ["Courtesy of Carver 4-County Museum"] }
 },
 "items": [
 {
 "id": "https://example.org/iiif/item-123/canvas/p1",
 "type": "Canvas",
 "width": 3000,
 "height": 4000,
 "items": [
 {
 "id": "https://example.org/iiif/item-123/canvas/p1/anno/image/page",
 "type": "AnnotationPage",
 "items": [
 {
 "id": "https://example.org/iiif/item-123/canvas/p1/anno/image",
 "type": "Annotation",
 "motivation": "painting",
 "body": {
 "id": "https://s3.example.org/scans/item-123/page-1.jpg",
 "type": "Image",
 "format": "image/jpeg",
 "width": 3000,
 "height": 4000,
 "service": [
 {
 "id": "https://iiif.example.org/image/item-123-page-1",
 "type": "ImageService3",
 "profile": "level2"
 }
 ]
 },
 "target": "https://example.org/iiif/item-123/canvas/p1"
 }
 ]
 }
 ],
 "annotations": [
 {
 "id": "https://example.org/iiif/item-123/canvas/p1/ocr/fulltext/page",
 "type": "AnnotationPage",
 "items": [
 {
 "id": "https://example.org/iiif/item-123/canvas/p1/ocr/fulltext",
 "type": "Annotation",
 "motivation": "supplementing",
 "body": {
 "type": "TextualBody",
 "value": "The full OCR text of page 1...",
 "language": "en",
 "format": "text/plain"
 },
 "target": "https://example.org/iiif/item-123/canvas/p1"
 }
 ]
 }
 ]
 }
 ],
 "service": [
 {
 "id": "https://example.org/iiif/item-123/search",
 "type": "SearchService2",
 "profile": "http://iiif.io/api/search/2/service"
 }
 ]
}

Model Classes

All model classes implement JsonSerializable and use public properties with no boilerplate getters/setters.

Core Resources

  • AbstractResource - Base class with common properties (id, type, label, summary, metadata, rights, etc.)
  • Manifest - IIIF Manifest with items (Canvases) and structures (Ranges)
  • Collection - Collection of Manifests or other Collections
  • Canvas - A canvas that contains annotations (images, OCR text)
  • Range - For table of contents / structural navigation (TOC)
  • AnnotationPage - Container for Annotations
  • Annotation - W3C Web Annotation with motivation, body, and target

Content/Body Classes

  • ResourceItem - Image, Video, Audio with id, type, format, service
  • TextualBody - Inline text content (OCR text, descriptions)

Supporting Classes

  • Service - IIIF Service reference
  • ImageService3 - Convenience class for Image Service 3
  • Thumbnail - Simplified image reference
  • LabelMap - Helper for language map construction
  • MetadataEntry - label/value pair for metadata

Enums

  • Motivation - painting, supplementing, commenting, tagging, etc.
  • ViewingDirection - left-to-right, right-to-left, top-to-bottom, bottom-to-top
  • Behavior - paged, continuous, individuals, auto-advance, etc.

Manual Construction

You can also construct manifests manually without using the builder:

use Survos\IiifBundle\Model\Manifest;
use Survos\IiifBundle\Model\Canvas;
use Survos\IiifBundle\Model\AnnotationPage;
use Survos\IiifBundle\Model\Annotation;
use Survos\IiifBundle\Model\ResourceItem;
use Survos\IiifBundle\Model\LabelMap;
use Survos\IiifBundle\Enum\Motivation;

$manifest = Manifest::create('https://example.org/manifest');
$manifest->setLabel('en', 'My Document');

$canvas = Canvas::create(
 'https://example.org/canvas/1',
 LabelMap::create('en', 'Page 1'),
 3000,
 4000
);

$annotation = Annotation::createPainting(
 'https://example.org/annotation/1',
 ResourceItem::createImage('https://example.org/image1.jpg', 'image/jpeg', 3000, 4000),
 'https://example.org/canvas/1'
);

$annotationPage = AnnotationPage::create('https://example.org/annopage/1');
$annotationPage->addItem($annotation);
$canvas->addItem($annotationPage);

$manifest->addItem($canvas);

$json = json_encode(['@context' => 'http://iiif.io/api/presentation/3/context.json'] + $manifest->jsonSerialize(), JSON_PRETTY_PRINT);

Requirements

  • PHP 8.4+
  • Symfony 7.3+ / 8.0+

Viewers

The <twig:iiif:viewer> Twig component offers two embedded viewers:

  • viewer="openseadragon" (default) β€” deep-zoom of a single image, or paging through plain image URLs (no IIIF Image API tile server needed).
  • viewer="diva" β€” the diva.js 7.2.6 page-turning document viewer (built on OpenSeadragon), for multi-page documents. It parses IIIF Presentation 2.x and 3.x and re-dispatches diva's page/zoom/loading events as bubbling iiif-diva:* Stimulus events so a host page can show per-page OCR or tags.

See docs/diva-viewer.md for the diva integration details (OpenSeadragon peer dependency, constructor settings, and the event wiring).

References