Skip to content

Commit

Permalink
Merge pull request #299 from digirati-co-uk/feature/image-annotations
Browse files Browse the repository at this point in the history
Image with caption annotation type
  • Loading branch information
stephenwf authored Dec 19, 2024
2 parents 47c92d6 + 029cba8 commit 5d3e794
Show file tree
Hide file tree
Showing 10 changed files with 211 additions and 25 deletions.
2 changes: 1 addition & 1 deletion apps/web/next-env.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,4 @@
/// <reference types="next/image-types/global" />

// NOTE: This file should not be edited
// see https://nextjs.org/docs/basic-features/typescript for more information.
// see https://nextjs.org/docs/app/building-your-application/configuring/typescript for more information.
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
"scripts": {
"build": "turbo build",
"build-packages": "turbo build --filter=@manifest-editor/* --filter=manifest-editor",
"dev": "turbo dev",
"dev": "turbo dev --filter=web... --concurrency 15",
"lint": "turbo lint",
"format": "prettier --write \"**/*.{ts,tsx,md}\"",
"mismatch": "syncpack list-mismatches",
Expand Down
8 changes: 4 additions & 4 deletions packages/components/src/ManifestOverviewEmptyState.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,10 @@ export function ManifestOverviewEmptyState({ onCreate, canCreate }: { onCreate:
onClick={canCreate ? onCreate : undefined}
className="border-2 border-dotted bg-black/5 aspect-[3/4] rounded w-32 border-black"
/>
<div className="font-bold text-lg">Good to go!</div>
<div className="max-w-xl px-5">
Add a title and top level information for this manifest using the editing panel, and then add some canvases and
content.
<div className="font-bold text-lg">Let's get started!</div>
<div className="max-w-xl px-5 my-4">
Add a title and top level information for this manifest using the editing panel on the right, and then add some
canvases and content.
</div>
{canCreate ? (
<Button
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,140 @@
import type { InternationalString } from "@iiif/presentation-3";
import type { CreatorContext, CreatorFunctionContext } from "@manifest-editor/creator-api";
import { Input, InputContainer, InputLabel, LanguageFieldEditor } from "@manifest-editor/editors";
import { Button } from "@manifest-editor/ui/atoms/Button";
import { PaddedSidebarContainer } from "@manifest-editor/ui/atoms/PaddedSidebarContainer";
import { type FormEvent, useState } from "react";

export interface CreateCaptionedImageAnnotationPayload {
label?: InternationalString;
body: InternationalString;
imageUrl: string;
motivation?: string;
height?: number;
width?: number;
}

export async function createCaptionedImageAnnotation(
data: CreateCaptionedImageAnnotationPayload,
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" } }
)
);
}
}

const resource = await ctx.create(
"@manifest-editor/image-url-creator",
{ url: data.imageUrl },
{
parent: { resource: annotation, property: "body" },
}
);

if (resource) {
bodies.push(resource);
}

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

// @todo this might not make sense to be a canvas.
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],
});
}
}

export function CreateCaptionedImageAnnotation(props: CreatorContext<CreateCaptionedImageAnnotationPayload>) {
const [body, setBody] = useState<InternationalString>({ en: [""] });

const onSubmit = (e: FormEvent) => {
e.preventDefault();
const data = new FormData(e.target as HTMLFormElement);
const formData = Object.fromEntries(data.entries()) as any;

props.runCreate({
body,
imageUrl: formData.url,
});
};

return (
<form onSubmit={onSubmit}>
<PaddedSidebarContainer>
<InputContainer>
<InputContainer $wide>
<InputLabel htmlFor="id">Link to Image</InputLabel>
<Input id="url" name="url" defaultValue="" />
</InputContainer>
</InputContainer>

<InputContainer>
<InputLabel>Image Caption</InputLabel>
<LanguageFieldEditor
focusId={"html-content"}
label={"HTML Content"}
fields={body}
onSave={(e: any) => setBody(e.toInternationalString())}
/>
</InputContainer>

<Button type="submit">Create</Button>
</PaddedSidebarContainer>
</form>
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import { AddImageIcon, HTMLIcon } from "@manifest-editor/components";
import type { CreatorDefinition } from "@manifest-editor/creator-api";
import { CreateCaptionedImageAnnotation, createCaptionedImageAnnotation } from "./create-captioned-image-annotation";

export const captionedImageAnnotation: CreatorDefinition = {
id: "@manifest-editor/captioned-image-annotation",
create: createCaptionedImageAnnotation,
label: "Captioned image",
summary: "Add an image from a URL with caption",
icon: <AddImageIcon />,
render(ctx) {
return <CreateCaptionedImageAnnotation {...ctx} />;
},
resourceType: "Annotation",
resourceFields: ["id", "type", "motivation", "body", "target"],
additionalTypes: ["Canvas"],
supports: {
initialData: true,
parentTypes: ["AnnotationPage"],
parentFields: ["items"],
},
staticFields: {
type: "Annotation",
},
};
27 changes: 15 additions & 12 deletions packages/creators/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,19 +1,20 @@
import { emptyCanvas } from "./Canvas/EmptyCanvas";
import { plaintextCreator } from "./ContentResource/PlaintextCreator";
import { webPageCreator } from "./ContentResource/WebPageCreator";
import { htmlBodyCreator } from "./ContentResource/HTMLBodyCreator";
import { audioAnnotation } from "./Annotation/AudioAnnotation";
import { captionedImageAnnotation } from "./Annotation/CaptionedImageAnnotation";
import { htmlAnnotation } from "./Annotation/HTMLAnnotation";
import { emptyAnnotationPage } from "./AnnotationPage/EmptyAnnotationPage";
import { youTubeBodyCreator } from "./ContentResource/YouTubeCreator";
import { imageUrlCreator } from "./ContentResource/ImageUrlCreator";
import { imageServiceCreator } from "./ContentResource/ImageServiceCreator";
import { imageServiceAnnotation } from "./Annotation/ImageServiceAnnotation";
import { noBodyAnnotation } from "./Annotation/NoBodyAnnotation";
import { iiifBrowserCreator } from "./ContentResource/IIIFBrowserCreator";
import { internalCanvas } from "./Canvas/InternalCanvas";
import { imageUrlAnnotation } from "./Annotation/ImageUrlAnnotation";
import { audioAnnotation } from "./Annotation/AudioAnnotation";
import { noBodyAnnotation } from "./Annotation/NoBodyAnnotation";
import { videoAnnotation } from "./Annotation/VideoAnnotation";
import { emptyAnnotationPage } from "./AnnotationPage/EmptyAnnotationPage";
import { emptyCanvas } from "./Canvas/EmptyCanvas";
import { internalCanvas } from "./Canvas/InternalCanvas";
import { htmlBodyCreator } from "./ContentResource/HTMLBodyCreator";
import { iiifBrowserCreator } from "./ContentResource/IIIFBrowserCreator";
import { imageServiceCreator } from "./ContentResource/ImageServiceCreator";
import { imageUrlCreator } from "./ContentResource/ImageUrlCreator";
import { plaintextCreator } from "./ContentResource/PlaintextCreator";
import { webPageCreator } from "./ContentResource/WebPageCreator";
import { youTubeBodyCreator } from "./ContentResource/YouTubeCreator";
import { manifestBrowserCreator } from "./Manifest/ManifestBrowserCreator";

export const allCreators = [
Expand All @@ -34,6 +35,7 @@ export const allCreators = [
emptyAnnotationPage,
noBodyAnnotation,
internalCanvas,
captionedImageAnnotation,
];

export {
Expand All @@ -53,6 +55,7 @@ export {
imageUrlAnnotation,
audioAnnotation,
videoAnnotation,
captionedImageAnnotation,
// Not included
manifestBrowserCreator,
};
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ const decorator = new CompositeDecorator([
]);

export function RichTextLanguageField(props: RichTextLanguageField) {
const isHtml = props.value[0] === "<";
const isHtml = (props.value || [])[0] === "<";
const editorRef = useRef<Editor>(null);
const [htmlMode, setHtmlMode] = useState(isHtml);
const [focus, _setIsFocused] = useState(false);
Expand Down
14 changes: 10 additions & 4 deletions packages/editors/src/definitions/HTMLBodyEditor/HTMLEditor.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Reference } from "@iiif/presentation-3";
import type { Reference } from "@iiif/presentation-3";
import { useConfig, useEditor, useGenericEditor } from "@manifest-editor/shell";
import { PaddedSidebarContainer } from "@manifest-editor/ui/atoms/PaddedSidebarContainer";
import { InputContainer, InputLabel } from "../../components/Input";
Expand All @@ -13,7 +13,7 @@ export function HTMLEditor() {
return (
<PaddedSidebarContainer>
{body.map((item) => (
<HTMLEditorItem item={item as any} />
<HTMLEditorItem key={item.id} item={item as any} />
))}
</PaddedSidebarContainer>
);
Expand All @@ -25,7 +25,13 @@ function HTMLEditorItem({ item }: { item: Reference }) {

const { textGranularity } = editor.extensions;
const { language, value } = editor.descriptive;
const { motivation } = editor.technical;
const { motivation, mediaType } = editor.technical;

const mt = mediaType.get();

if (mt === "Image") {
return <img src={item.id} alt="" />;
}

return (
<>
Expand All @@ -41,7 +47,7 @@ function HTMLEditorItem({ item }: { item: Reference }) {
/>
</InputContainer>

{motivation.get() !== "painting" ? (
{i18n.textGranularityEnabled && motivation.get() !== "painting" ? (
<TextGranularityEditor
focusId={textGranularity.focusId()}
value={textGranularity.get()}
Expand Down
5 changes: 3 additions & 2 deletions packages/shell/src/ConfigContext/ConfigContext.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { PreviewConfiguration } from "../PreviewContext/PreviewContext.types";
import { createContext, ReactNode, useCallback, useContext, useMemo, useState } from "react";
import { Collection } from "@iiif/presentation-3";
import { ReactNode, createContext, useCallback, useContext, useMemo, useState } from "react";
import { PreviewConfiguration } from "../PreviewContext/PreviewContext.types";

export interface Config {
// Previous configuration.
Expand Down Expand Up @@ -32,6 +32,7 @@ export interface Config {
defaultLanguage: string;
availableLanguages: string[];
advancedLanguageMode: boolean;
textGranularityEnabled?: boolean;
};

// Options when exporting from Vault.
Expand Down
11 changes: 11 additions & 0 deletions packages/shell/src/ConfigContext/ConfigEditor.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,17 @@ export function ConfigEditor() {
/>
<Form.Label htmlFor="advancedLanguageMode">Advanced Language Mode</Form.Label>
</Form.InputContainer>


<Form.InputContainer horizontal className="my-3">
<Form.Input
type="checkbox"
name="textGranularityEnabled"
id="textGranularityEnabled"
defaultChecked={config.i18n?.textGranularityEnabled || false}
/>
<Form.Label htmlFor="textGranularityEnabled">Enable text granularity</Form.Label>
</Form.InputContainer>

<Form.InputContainer horizontal className="my-3">
<Form.Input
Expand Down

0 comments on commit 5d3e794

Please sign in to comment.