GithubHelp home page GithubHelp logo

iiif-builder's Introduction

IIIF Builder

Helper for creating IIIF Manifests and Collections.

npm install @iiif/builder
yarn add @iiif/builder

Or add a script tag.

<script src="https://cdn.jsdelivr.net/npm/@iiif/builder/dist/index.umd.js"></script>

Note: This uses global variable. You can create a builder with const builder = new IIIFBuilder.IIIFBuilder()

Example

import { IIIFBuilder } from '@iiif/builder';

const builder = new IIIFBuilder();
const newManifest = builder.createManifest(
  'https://iiif.io/api/cookbook/recipe/0001-mvm-image/manifest.json',
  (manifest) => {
    manifest.addLabel('Image 1', 'en');
    manifest.createCanvas('https://iiif.io/api/cookbook/recipe/0001-mvm-image/canvas/p1', (canvas) => {
      canvas.width = 1800;
      canvas.height = 1200;
      canvas.createAnnotation('https://iiif.io/api/cookbook/recipe/0001-mvm-image/annotation/p0001-image', {
        id: 'https://iiif.io/api/cookbook/recipe/0001-mvm-image/annotation/p0001-image',
        type: 'Annotation',
        motivation: 'painting',
        body: {
          id: 'http://iiif.io/api/presentation/2.1/example/fixtures/resources/page1-full.png',
          type: 'Image',
          format: 'image/png',
          height: 1800,
          width: 1200,
        },
      });
    });
  }
);

const jsonManifest = builder.toPresentation3({ id: newManifest.id, type: 'Manifest' });

BaseEntityBuilder

Most of the Editing functionality comes from the BaseEntityBuilder. This is a class that other builder extend to provide the common IIIF properties together.

The base builder has the following class properties and constructor:

class BaseEntityBuilder {
  entity: T;
  protected modified: Set<string> = new Set();
  protected newInstances: BaseEntityBuilder<any>[] = [];
  protected editedInstances: BaseEntityBuilder<any>[] = [];
  protected embeddedInstances: any[] = [];

  constructor(builder: IIIFBuilder, entity: T);
}

The entity is NormalizedResource. This will be stored directly in vault. You can shallow clone one of the emptyXYZ from the @iiif/parser pacakge.

For example:

import { emptyCanvas } from '@iiif/parser';

const builder = new IIIFBuilder();
const canvas = new BaseEntityBuilder(builder, { ...emptyCanvas, id: 'http://example.org/my-canvas' });

canvas.setLabel( ... );
// ..etc

Each of the properties is useful for nesting these builders.

base.modified

This is a list of keys of modified properties. For example, if you did:

canvas.setLabel({ en: ['...'] });

Then the value of canvas.modified would be: ['label']

base.newInstances

This is a list of BaseEntityBuilder classes for nested items. When you save the resource, all of these will be recursively checked and the changes applied.

For example, this is the helper for creating an embedded Canvas in the ManifestBuilder.

createCanvas(id: string, callback: (canvas: CanvasInstanceBuilder) => void) {
  // We create the builder for the canvas.
  const canvasBuilder = new CanvasInstanceBuilder(this.builder, { ...emptyCanvas, id });

  // We pass that to the callback provided (where the user will be setting canvas properties)
  callback(canvasBuilder);

  // We push the new instance, so it will be detected.
  this.newInstances.push(canvasBuilder);

  // We mark the `entity.items` as modified.
  this.modified.add('items');

  // And finally we directly modify the entity with our new Canvas.
  this.entity.items = [
    ...this.entity.items,
    {
      id,
      type: 'Canvas',
    },
  ];
}

base.editedInstances

This is similar to the new instances, but assumes that the resource already exists in the Vault, and only changes that have been made should be applied.

Here is the helper for editing an existing Canvas in the ManifestBuilder:

editCanvas(id: string, callback: (canvas: CanvasInstanceBuilder) => void) {
  // We grab the canvas from the Vault instance.
  const canvasToEdit = this.builder.vault.get<CanvasNormalized>({ id, type: 'Canvas' });

  // We create a new builder, with a shallow-clone of the properties
  const canvasBuilder = new CanvasInstanceBuilder(this.builder, { ...canvasToEdit });

  // We pass it back for editing
  callback(canvasBuilder);

  // And finally we push to newInstances (? - this might be a bug or a quirk!)
  this.newInstances.push(canvasBuilder);
}

base.embeddedInstances

This is for non-builder resources. For example, there is builder for "ContentResources" like images. So when you add a thumbnail:

addThumbnail(resource: ContentResource) {
  this.modified.add('thumbnail');
  this.entity.thumbnail = [...this.entity.thumbnail, this.addEmbeddedInstance(resource, 'ContentResource')];
}

We can use the addEmbeddedInstance and then this will be created when you build. It returns a Ref that can be used for convenience.

This is a useful shortcut for simple types that don't need a full builder.

iiif-builder's People

Contributors

stephenwf avatar adamjarling avatar mathewjordan avatar

Stargazers

 avatar Sebastian Pałucha avatar Martim Passos avatar Johannes Baiter avatar Tarje Lavik avatar bruno avatar Kelsie Caldwell avatar Mark Baggett avatar  avatar marii¨ avatar raffaele messuti avatar  avatar

Watchers

James Cloos avatar  avatar  avatar Kelsie Caldwell avatar

Forkers

nulib

iiif-builder's Issues

Builder recipes

During work on the Manifest Editor, some new patterns for create IIIF were created. Usually focussed on creating IIIF from users input. At the moment it's tied to that UI layer, but I think it could be useful as a standalone piece of iiif builder.

Examples

Here is a minimal definition:

interface CreatePlaintextPayload {
  url: string;
  label?: InternationalString;
}

async function createPlaintext(data: CreatePlaintextPayload, ctx: CreatorFunctionContext) {
  return ctx.embed({
    id: data.url,
    type: "Text",
    label: data.label,
    format: "text/plain",
  });
}

export const plaintextCreator: CreatorDefinition = {
  id: "@manifest-editor/plaintext-creator",
  create: createPlaintext,
  label: "Plaintext",
  summary: "Add link to an plaintext",
  resourceType: "ContentResource",
  resourceFields: ["label", "format"],
  supports: {
    parentFields: ["seeAlso", "rendering", "homepage"],
  },
  staticFields: {
    format: "text/plain",
  },
};

It fully describes taking user input:

{
  "id": "https://example.org/some-link",
  "label": { "en": ["Some link"] }
}

And producing:

{
  "id": "https://example.org/some-link",
  "type": "Text",
  "label": { "en": ["Some link"] },
  "format": "text/plain"
}

This example is not a groundbreaking abstraction. But it does allow for more information to be attached. For example, human readable label/summary for what will be created, where this resource is valid from a IIIF point of view, which fields are created - and which are static.

Nesting definitions

Having a "Library" of these definitions allows the Manifest Editor to compose them together.

interface Payload {
  label?: InternationalString;
  body: InternationalString;
  motivation?: string;
  height?: number;
  width?: number;
}

async function createHtmlAnnotation(data: Payload, ctx: CreatorFunctionContext) {
  const annotation = {
    id: ctx.generateId("annotation"),
    type: "Annotation",
  };

  const targetType = ctx.options.targetType as "Annotation" | "Canvas";

  const languages = Object.keys(data.body);
  const bodies = [];
  for (const language of languages) {
    const body = (data.body as any)[language].join("\n");
    if (body) {
      bodies.push(
        await ctx.create(
          "@manifest-editor/html-body-creator",
          {
            language,
            body,
          },
          { parent: { resource: annotation, property: "items" } }
        )
      );
    }
  }

  if (targetType === "Annotation") {
    return ctx.embed({
      ...annotation,
      motivation: data.motivation || ctx.options.initialData?.motivation || "painting",
      body: bodies,
      target: ctx.getTarget(),
    });
  }

  if (targetType === "Canvas") {
    const canvasId = ctx.generateId("canvas");
    const pageId = ctx.generateId("annotation-page", { id: canvasId, type: "Canvas" });

    const annotationResource = ctx.embed({
      ...annotation,
      motivation: "painting",
      body: bodies,
      target: { type: "SpecificResource", source: { id: canvasId, type: "Canvas" } },
    });

    const page = ctx.embed({
      id: pageId,
      type: "AnnotationPage",
      items: [annotationResource],
    });

    return ctx.embed({
      id: canvasId,
      type: "Canvas",
      label: data.label || { en: ["Untitled HTML canvas"] },
      height: data.height || 1000,
      width: data.width || 1000,
      items: [page],
    });
  }
}

There is a lot going on in this one, but if you look at the definition you can see what it supports:

export const htmlAnnotation: CreatorDefinition = {
  id: "@manifest-editor/html-annotation",
  create: createHtmlAnnotation,
  label: "HTML Annotation",
  summary: "Add HTML annotation",
  resourceType: "Annotation",
  resourceFields: ["id", "type", "motivation", "body", "target"],
  additionalTypes: ["Canvas"],
  supports: {
    initialData: true,
    parentTypes: ["AnnotationPage", "Manifest"],
    parentFields: ["items"],
  },
  staticFields: {
    type: "Annotation",
  },
};

So this produces an Annotation and can be created on:

  • AnnotationPage - Annotation added directly to the page
  • Manifest - A new canvas is created, and the annotation added

In the creator example, there is a "nested" example, which is calling out to another definition:

await ctx.create(
  "@manifest-editor/html-body-creator",
  {
    language,
    body,
  },
  { parent: { resource: annotation, property: "items" } }
);

So you can compose them together.

With IIIF Builder, you could install or create "plugins" and use them to build IIIF either statically, or incrementally in a UI, similar to the Manifest Editor.

Creating `canvas.annotations`

Hey Stephen,

We're looking into using iiif-builder for on-the-fly Manifest creation.

One thing we'd like to do is add an annotations property (https://iiif.io/api/presentation/3.0/#annotations) of of any Canvas. I'm wondering if you have a method for doing this already? I've tried a couple different avenues but nothing seemed to work.

You're familiar with https://iiif.io/api/cookbook/recipe/0219-using-caption-file/ and the structure we're seeking is pretty similar.

{
{
  "id": "https://iiif.io/api/cookbook/recipe/0219-using-caption-file/canvas",
  "type": "Canvas",
  "height": 360,
  "width": 480,
  "duration": 572.034,
  "items": [],
  "annotations": [
    {
      "id": "https://iiif.io/api/cookbook/recipe/0219-using-caption-file/canvas/page2",
      "type": "AnnotationPage",
      "items": [
        {
          "id": "https://iiif.io/api/cookbook/recipe/0219-using-caption-file/canvas/page2/a1",
          "type": "Annotation",
          "motivation": "supplementing",
          "body": {
            "id": "https://fixtures.iiif.io/video/indiana/lunchroom_manners/lunchroom_manners.vtt",
            "type": "Text",
            "format": "text/vtt",
            "label": {
              "en": [
                "Captions in WebVTT format"
              ]
            },
            "language": "en"
          },
          "target": "https://iiif.io/api/cookbook/recipe/0219-using-caption-file/canvas"
        }
      ]
    }
  ]
}

Add `provider` and add properties

Hey Stephen,

Currently it seems the best way to add provider is to append after a resource is built and converted to 3.0.

The nature of provider https://iiif.io/api/presentation/3.0/#provider is interesting. Should both Collections, Manifests, and Canvases be able to have an addProvider function? Understanding the structure of provider is a bit heavy with label, homepage, logo, and seeAlso, this might be less than easy.

Best,
Mat

Potential for adding more propeties to `partOf` entry

Hey Stephen,

An idea I've had for a while is to be able to extend a partOf entry of a Manifest with some additional properties. Doing so would allow the Manifest to carry some information for the Collection it may be a part of.

{
  "partOf": [
    {
      "id": "https://acw5dcf49d.execute-api.us-east-1.amazonaws.com/dev/collections/iiif-image-collection",
      "type": "Collection",
      "label": { "none": ["Commedia dell'Arte: The Masks of Antonio Fava"] },
      "summary": {
        "none": [
          "The Commedia dell'Arte, the famous improvisational theatre style born in Renaissance Italy, remains a major influence in today's theatre. Antonio Fava is an actor, comedian, author, director, musician, mask maker and Internationally renowned Maestro of Commedia dell'Arte."
        ]
      },
      "homepage": [
        {
          "id": "https://dc.library.northwestern.edu/collections/c373ecd2-2c45-45f2-9f9e-52dc244870bd",
          "type": "Text",
          "label": {
            "none": ["Commedia dell'Arte: The Masks of Antonio Fava"]
          },
          "format": "text/html"
        }
      ],
      "thumbnail": [
        {
          "id": "https://iiif.stack.rdc.library.northwestern.edu/iiif/2/180682c9-dfaf-4881-b7b6-1f2f21092d4f/full/200,/0/default.jpg",
          "type": "Image",
          "format": "image/jpeg",
          "service": [
            {
              "id": "https://iiif.stack.rdc.library.northwestern.edu/iiif/2/180682c9-dfaf-4881-b7b6-1f2f21092d4f",
              "profile": "http://iiif.io/api/image/2/level2.json",
              "type": "ImageService2"
            }
          ],
          "width": 200,
          "height": 200
        }
      ]
    }
  ]
}

Currently, it seems like the partOf property strips out homepage and does an $UNSET for thumbnail. I'd love to gauge your thoughts on this but I do wonder how possible it might be to allow some of these properties.

Create annotation with target region

Using the IIIF Builder, I'm unable to create annotations with a target region (example: <canvas-id>#xywh=500,500,500,500). It looks like the default annotation target is always used, whether or not a target is provided.

Example:

const builder = new IIIFBuilder();
const newManifest = builder.createManifest(
  "http://localhost:8080/iiif/fig-032/manifest.json",
  (manifest) => {
    manifest.addLabel("Image 1", "en");
    manifest.createCanvas(
      "http://localhost:8080/iiif/fig-032/canvas",
      (canvas) => {
        canvas.width = 1800;
        canvas.height = 1200;
        canvas.createAnnotation(
          "http://localhost:8080/iiif/fig-032/canvas/wax-to-wax-joints",
          {
            body: {
              format: "image/png",
              height: 500,
              id: "http://localhost:8080/_assets/images/figures/wax-joints.png",
              label: { en: ["Wax Joints"] },
              type: "Image",
              width: 500
            },
            id: "http://localhost:8080/iiif/fig-032/canvas/wax-to-wax-joints",
            motivation: "painting",
            target: "http://localhost:8080/iiif/fig-032/canvas#xywh=300,300,500,500",
            type: "Annotation"
          }
        );
      }
    );
  }
);

Commons migration

  • Re-added jest tests
  • Documentation folder
  • Automatic NPM releases
  • Extra tests (Creating cookbook examples?)
  • Migrate to vite / vitest

Support for creating collections within collections?

Hey @stephenwf. This is great stuff. I'm trying to use it to build collections on the fly and wondering if it can support building out collections with items of collections? Perhaps I'm just not making the correct call here.

When I attempt to call collection.createCollection, I'm getting the following:
Property 'createCollection' does not exist on type 'CollectionInstanceBuilder'.

collection.createManifest works like a charm. 👍

For my full code:

import { IIIFBuilder } from "iiif-builder";

const buildCollection = (data) => {
  const { id, label, summary, homepage, items } = data;

  const builder = new IIIFBuilder();

  return builder.createCollection(id, (collection) => {
    collection.addLabel(label, "none");
    collection.addSummary(summary, "none");
    collection.setHomepage({
      id: homepage,
    });
    items.forEach((item) => {
      collection.createCollection(item.id, (manifest) => {
        manifest.addLabel(item.label, "none");
        manifest.addSummary(item.summary, "none");
        manifest.setHomepage({
          id: homepage,
        });
        manifest.addThumbnail({
          id: "http://localhost:5001/...",
        });
      });
    });
  });
};

export { buildCollection };

Annotation builder

Currently there is no Annotation builder, you have to provide the JSON for each annotation individually.

I don't think a builder in the style of the Canvas or Manifest builders would for this case - instead maybe a set of presets.

Annotation.imageWithService(
  'https://example.org/image.jpg',
  { service: 'https://example.org/image.jpg/info.json' }
);

What would be nice is to include some of the analysis work done in Manifest editor. Where you can provide just an Image URL or Image service and it will figure out what the ID and inline service block should look like. This would be async and would be opt-in (with an await).

For example, the following URL:

https://iiif.wellcomecollection.org/image/b18035723_0004.JP2/full/732,1024/0/default.jpg

Might return:

{
  "id": "https://example.org/canvas/354e4ae3-d4bc-4785-b567-77dfd65bf9f8/painting",
  "type": "Annotation",
  "motivation": "painting",
  "target": "https://example.org/canvas/354e4ae3-d4bc-4785-b567-77dfd65bf9f8",
  "body": {
    "id": "https://example.org/image/4d0f3a1d-1170-42ed-81cd-49099be604f3",
    "type": "Image",
    "format": "image/jpg",
    "height": 3372,
    "width": 2411,
    "service": [
      {
        "@context": "http://iiif.io/api/image/2/context.json",
        "@id": "https://iiif.wellcomecollection.org/image/b18035723_0004.JP2",
        "protocol": "http://iiif.io/api/image",
        "width": 2411,
        "height": 3372,
        "tiles": [
          {
            "width": 256,
            "height": 256,
            "scaleFactors": [
              1,
              2,
              4,
              8,
              16
            ]
          }
        ],
        "sizes": [
          {
            "width": 715,
            "height": 1000
          },
          {
            "width": 286,
            "height": 400
          },
          {
            "width": 143,
            "height": 200
          },
          {
            "width": 72,
            "height": 100
          }
        ],
        "profile": [
          "http://iiif.io/api/image/2/level1.json",
          {
            "formats": [
              "jpg"
            ],
            "qualities": [
              "native",
              "color",
              "gray"
            ],
            "supports": [
              "regionByPct",
              "sizeByForcedWh",
              "sizeByWh",
              "sizeAboveFull",
              "rotationBy90s",
              "mirroring",
              "gray"
            ]
          }
        ],
        "type": "ImageService"
      }
    ]
  }
}

Extra configuration could limit how much of the resolved service is inlined.

Other helpers for common types of annotations could then be created.

Annotation `body.value` gets ignored

I'm creating annotations with

canvas.createAnnotationPage(annoPageId, (annoPage) => {
            const buildAnnotation = (canvasId, annoIndex, value) => {
              annoPage.createAnnotation({
                "id": `${canvasId}/annopage-2/anno-${annoIndex}`, 
                "type": "Annotation",
                "motivation": "commenting",
                "body": {
                  "type": "TextualBody",
                  "format": "text/html",
                  "value": value.note ? value.note : value,
                },
                "target": (value.x && value.y && value.width && value.height)
                  ? `${canvasId}#xywh=${value.x},${value.y},${value.width},${value.height}`
                  : canvasId
              })
            }
...

It works fine except that value (strings such as "<p>Some annotation</p>") disappears in the output manifests. Am I doing something wrong or is some type check failing here?

Issue creating annotation with `body.items`

When creating an annotation with body.items, the items are not returned.

Example for reproduction:

const manifest = builder.createManifest(
  "https://preview.iiif.io/cookbook/3333-choice/recipe/0033-choice/manifest.json",
  (manifest) => {
    manifest.addLabel(
      "John Dee performing an experiment before Queen Elizabeth I.",
      "en"
    );
    manifest.createCanvas(
      "https://preview.iiif.io/cookbook/3333-choice/recipe/0033-choice/canvas/p1",
      (canvas) => {
        canvas.height = 1271;
        canvas.width = 2000;
        canvas.createAnnotation(
          "https://preview.iiif.io/cookbook/3333-choice/recipe/0033-choice/annotation/p0001-image",
          {
            id:
              "https://preview.iiif.io/cookbook/3333-choice/recipe/0033-choice/annotation/p0001-image",
            type: "Annotation",
            motivation: "painting",
            body: {
              type: "Choice",
              items: [
                {
                  id:
                    "https://iiif.io/api/image/3.0/example/reference/421e65be2ce95439b3ad6ef1f2ab87a9-dee-natural/full/max/0/default.jpg",
                  type: "Image",
                  format: "image/jpeg",
                  width: 2000,
                  height: 1271,
                  label: {
                    en: ["Natural Light"]
                  },
                },
                {
                  id:
                    "https://iiif.io/api/image/3.0/example/reference/421e65be2ce95439b3ad6ef1f2ab87a9-dee-xray/full/max/0/default.jpg",
                  type: "Image",
                  format: "image/jpeg",
                  width: 2000,
                  height: 1271,
                  label: {
                    en: ["X-Ray"]
                  }
                }
              ]
            }
          }
        )
      }
    )
  }
)

This otherwise creates a manifest as expected, but the annotation body will be missing the items property:

{
  ...
    "body": [{ "type": "Choice" }]
  ...
}

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.