Spaces:
Build error
feat: IIIF-native Sprints 4-5-6 — tiled zoom, exports, cleanup
Browse filesSprint IIIF-4 — Frontend tiled zoom:
- Viewer.tsx: new props iiifServiceUrl + fallbackImageUrl. When
iiifServiceUrl is set, opens via info.json for native IIIF tiled
zoom (progressive tile loading). Falls back to simple image loading.
Added crossOriginPolicy for CORS-enabled IIIF servers.
- Reader.tsx: passes iiif_service_url from page data to Viewer.
- Editor.tsx: passes iiif_service_url from master ImageInfo to Viewer.
Sprint IIIF-5 — Exports with IIIF URLs:
- iiif.py: generated manifest now includes "service" block on annotation
body when iiif_service_url is set (ImageService3, level2). Any IIIF
viewer (Mirador, Universal Viewer) can use tiled zoom.
- mets.py: fileSec uses IIIF URLs (LOCTYPE="URL") for master and
derivative when iiif_service_url available. Legacy filepath fallback.
- alto.py: fileName prefers iiif_service_url over local paths.
Sprint IIIF-6 — Documentation:
- CLAUDE.md: updated data/ directory notes explaining that masters/,
derivatives/, thumbnails/ are empty in IIIF-native mode.
585 tests pass, 0 regressions. TypeScript clean.
https://claude.ai/code/session_01UB4he7RdRPHLvNjky4X8Sw
|
@@ -159,9 +159,12 @@ iiif-studio/
|
|
| 159 |
├── data/ ← JAMAIS versionné (.gitignore)
|
| 160 |
│ └── corpora/
|
| 161 |
│ └── {corpus_slug}/
|
| 162 |
-
│ ├── masters/ ← images
|
| 163 |
-
│ ├── derivatives/ ← JPEG 1500px
|
| 164 |
-
│ ├── thumbnails/ ← aperçus 300px
|
|
|
|
|
|
|
|
|
|
| 165 |
│ ├── iiif/
|
| 166 |
│ │ ├── manifest.json
|
| 167 |
│ │ └── annotations/
|
|
|
|
| 159 |
├── data/ ← JAMAIS versionné (.gitignore)
|
| 160 |
│ └── corpora/
|
| 161 |
│ └── {corpus_slug}/
|
| 162 |
+
│ ├── masters/ ← images uploadées (mode fichier uniquement)
|
| 163 |
+
│ ├── derivatives/ ← JPEG 1500px (mode fichier uniquement)
|
| 164 |
+
│ ├── thumbnails/ ← aperçus 300px (mode fichier uniquement)
|
| 165 |
+
│ │ NOTE : en mode IIIF natif, masters/, derivatives/ et
|
| 166 |
+
│ │ thumbnails/ sont VIDES — les images sont streamées
|
| 167 |
+
│ │ depuis le serveur IIIF d'origine.
|
| 168 |
│ ├── iiif/
|
| 169 |
│ │ ├── manifest.json
|
| 170 |
│ │ └── annotations/
|
|
@@ -160,7 +160,7 @@ def generate_alto(master: PageMaster) -> str:
|
|
| 160 |
etree.SubElement(desc, _a("MeasurementUnit")).text = "pixel"
|
| 161 |
|
| 162 |
src_info = etree.SubElement(desc, _a("sourceImageInformation"))
|
| 163 |
-
file_name = master.image.master or master.image.derivative_web or master.page_id
|
| 164 |
etree.SubElement(src_info, _a("fileName")).text = str(file_name)
|
| 165 |
|
| 166 |
if master.processing:
|
|
|
|
| 160 |
etree.SubElement(desc, _a("MeasurementUnit")).text = "pixel"
|
| 161 |
|
| 162 |
src_info = etree.SubElement(desc, _a("sourceImageInformation"))
|
| 163 |
+
file_name = master.image.iiif_service_url or master.image.master or master.image.derivative_web or master.page_id
|
| 164 |
etree.SubElement(src_info, _a("fileName")).text = str(file_name)
|
| 165 |
|
| 166 |
if master.processing:
|
|
@@ -108,6 +108,23 @@ def generate_manifest(
|
|
| 108 |
annotation_page_id = f"{canvas_id}/annotation-page/1"
|
| 109 |
annotation_id = f"{canvas_id}/annotation/painting"
|
| 110 |
image_url = page.image.master or ""
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 111 |
|
| 112 |
canvas: dict = {
|
| 113 |
"id": canvas_id,
|
|
@@ -124,14 +141,8 @@ def generate_manifest(
|
|
| 124 |
"id": annotation_id,
|
| 125 |
"type": "Annotation",
|
| 126 |
"motivation": "painting",
|
| 127 |
-
"body":
|
| 128 |
-
|
| 129 |
-
"type": "Image",
|
| 130 |
-
"format": "image/jpeg",
|
| 131 |
-
"width": width,
|
| 132 |
-
"height": height,
|
| 133 |
-
},
|
| 134 |
-
"target": canvas_id,
|
| 135 |
}
|
| 136 |
],
|
| 137 |
}
|
|
|
|
| 108 |
annotation_page_id = f"{canvas_id}/annotation-page/1"
|
| 109 |
annotation_id = f"{canvas_id}/annotation/painting"
|
| 110 |
image_url = page.image.master or ""
|
| 111 |
+
iiif_svc = page.image.iiif_service_url
|
| 112 |
+
|
| 113 |
+
# Corps de l'annotation painting
|
| 114 |
+
body: dict = {
|
| 115 |
+
"id": image_url,
|
| 116 |
+
"type": "Image",
|
| 117 |
+
"format": "image/jpeg",
|
| 118 |
+
"width": width,
|
| 119 |
+
"height": height,
|
| 120 |
+
}
|
| 121 |
+
# Si un IIIF Image Service est connu, le déclarer (zoom tuilé natif)
|
| 122 |
+
if iiif_svc:
|
| 123 |
+
body["service"] = [{
|
| 124 |
+
"id": iiif_svc,
|
| 125 |
+
"type": "ImageService3",
|
| 126 |
+
"profile": "level2",
|
| 127 |
+
}]
|
| 128 |
|
| 129 |
canvas: dict = {
|
| 130 |
"id": canvas_id,
|
|
|
|
| 141 |
"id": annotation_id,
|
| 142 |
"type": "Annotation",
|
| 143 |
"motivation": "painting",
|
| 144 |
+
"body": body,
|
| 145 |
+
"target": canvas_id,
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 146 |
}
|
| 147 |
],
|
| 148 |
}
|
|
@@ -178,20 +178,28 @@ def generate_mets(
|
|
| 178 |
for page in pages:
|
| 179 |
sid = _safe_id(page.page_id)
|
| 180 |
|
| 181 |
-
# master image
|
|
|
|
|
|
|
|
|
|
| 182 |
f_master = _el(grp_master, f"{_M}file", {"ID": f"IMG_MASTER_{sid}", "MIMETYPE": "image/jpeg"})
|
| 183 |
_el(f_master, f"{_M}FLocat", {
|
| 184 |
"LOCTYPE": "URL",
|
| 185 |
-
f"{_XL}href":
|
| 186 |
f"{_XL}type": "simple",
|
| 187 |
})
|
| 188 |
|
| 189 |
-
# dérivé web
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 190 |
f_deriv = _el(grp_deriv, f"{_M}file", {"ID": f"IMG_DERIV_{sid}", "MIMETYPE": "image/jpeg"})
|
| 191 |
_el(f_deriv, f"{_M}FLocat", {
|
| 192 |
-
|
| 193 |
-
"
|
| 194 |
-
f"{_XL}href": page.image.derivative_web or "",
|
| 195 |
f"{_XL}type": "simple",
|
| 196 |
})
|
| 197 |
|
|
|
|
| 178 |
for page in pages:
|
| 179 |
sid = _safe_id(page.page_id)
|
| 180 |
|
| 181 |
+
# master image (IIIF service URL ou URL statique)
|
| 182 |
+
master_url = page.image.iiif_service_url or page.image.master or ""
|
| 183 |
+
if page.image.iiif_service_url:
|
| 184 |
+
master_url = f"{page.image.iiif_service_url}/full/max/0/default.jpg"
|
| 185 |
f_master = _el(grp_master, f"{_M}file", {"ID": f"IMG_MASTER_{sid}", "MIMETYPE": "image/jpeg"})
|
| 186 |
_el(f_master, f"{_M}FLocat", {
|
| 187 |
"LOCTYPE": "URL",
|
| 188 |
+
f"{_XL}href": master_url,
|
| 189 |
f"{_XL}type": "simple",
|
| 190 |
})
|
| 191 |
|
| 192 |
+
# dérivé web (URL IIIF 1500px ou chemin local legacy)
|
| 193 |
+
if page.image.iiif_service_url:
|
| 194 |
+
deriv_href = f"{page.image.iiif_service_url}/full/!1500,1500/0/default.jpg"
|
| 195 |
+
deriv_loctype_attrs = {"LOCTYPE": "URL"}
|
| 196 |
+
else:
|
| 197 |
+
deriv_href = page.image.derivative_web or ""
|
| 198 |
+
deriv_loctype_attrs = {"LOCTYPE": "OTHER", "OTHERLOCTYPE": "filepath"}
|
| 199 |
f_deriv = _el(grp_deriv, f"{_M}file", {"ID": f"IMG_DERIV_{sid}", "MIMETYPE": "image/jpeg"})
|
| 200 |
_el(f_deriv, f"{_M}FLocat", {
|
| 201 |
+
**deriv_loctype_attrs,
|
| 202 |
+
f"{_XL}href": deriv_href,
|
|
|
|
| 203 |
f"{_XL}type": "simple",
|
| 204 |
})
|
| 205 |
|
|
@@ -3,14 +3,16 @@ import OpenSeadragon from 'openseadragon'
|
|
| 3 |
import { RetroButton } from './retro'
|
| 4 |
|
| 5 |
interface Props {
|
| 6 |
-
|
|
|
|
|
|
|
|
|
|
| 7 |
onViewerReady?: (viewer: OpenSeadragon.Viewer) => void
|
| 8 |
}
|
| 9 |
|
| 10 |
-
const Viewer: FC<Props> = ({
|
| 11 |
const containerRef = useRef<HTMLDivElement>(null)
|
| 12 |
const viewerRef = useRef<OpenSeadragon.Viewer | null>(null)
|
| 13 |
-
// Ref pour toujours accéder au callback le plus récent (évite stale closure)
|
| 14 |
const onViewerReadyRef = useRef(onViewerReady)
|
| 15 |
onViewerReadyRef.current = onViewerReady
|
| 16 |
|
|
@@ -25,6 +27,7 @@ const Viewer: FC<Props> = ({ imageUrl, onViewerReady }) => {
|
|
| 25 |
animationTime: 0.3,
|
| 26 |
minZoomLevel: 0.1,
|
| 27 |
maxZoomLevel: 20,
|
|
|
|
| 28 |
})
|
| 29 |
|
| 30 |
viewerRef.current = viewer
|
|
@@ -35,15 +38,25 @@ const Viewer: FC<Props> = ({ imageUrl, onViewerReady }) => {
|
|
| 35 |
}
|
| 36 |
}, [])
|
| 37 |
|
|
|
|
|
|
|
|
|
|
| 38 |
useEffect(() => {
|
| 39 |
const viewer = viewerRef.current
|
| 40 |
-
if (!viewer || !
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 41 |
|
| 42 |
-
viewer.open({ type: 'image', url: imageUrl })
|
| 43 |
viewer.addOnceHandler('open', () => {
|
| 44 |
onViewerReadyRef.current?.(viewer)
|
| 45 |
})
|
| 46 |
-
}, [
|
| 47 |
|
| 48 |
return (
|
| 49 |
<div className="relative w-full h-full bg-retro-black">
|
|
|
|
| 3 |
import { RetroButton } from './retro'
|
| 4 |
|
| 5 |
interface Props {
|
| 6 |
+
/** URL du IIIF Image Service (zoom tuilé natif) */
|
| 7 |
+
iiifServiceUrl?: string | null
|
| 8 |
+
/** URL image statique (fallback si pas de service IIIF) */
|
| 9 |
+
fallbackImageUrl?: string | null
|
| 10 |
onViewerReady?: (viewer: OpenSeadragon.Viewer) => void
|
| 11 |
}
|
| 12 |
|
| 13 |
+
const Viewer: FC<Props> = ({ iiifServiceUrl, fallbackImageUrl, onViewerReady }) => {
|
| 14 |
const containerRef = useRef<HTMLDivElement>(null)
|
| 15 |
const viewerRef = useRef<OpenSeadragon.Viewer | null>(null)
|
|
|
|
| 16 |
const onViewerReadyRef = useRef(onViewerReady)
|
| 17 |
onViewerReadyRef.current = onViewerReady
|
| 18 |
|
|
|
|
| 27 |
animationTime: 0.3,
|
| 28 |
minZoomLevel: 0.1,
|
| 29 |
maxZoomLevel: 20,
|
| 30 |
+
crossOriginPolicy: 'Anonymous',
|
| 31 |
})
|
| 32 |
|
| 33 |
viewerRef.current = viewer
|
|
|
|
| 38 |
}
|
| 39 |
}, [])
|
| 40 |
|
| 41 |
+
// Source à ouvrir : préférer le service IIIF (zoom tuilé), sinon image statique
|
| 42 |
+
const source = iiifServiceUrl || fallbackImageUrl || ''
|
| 43 |
+
|
| 44 |
useEffect(() => {
|
| 45 |
const viewer = viewerRef.current
|
| 46 |
+
if (!viewer || !source) return
|
| 47 |
+
|
| 48 |
+
if (iiifServiceUrl) {
|
| 49 |
+
// Zoom tuilé natif — OpenSeadragon fetch info.json et configure les tuiles
|
| 50 |
+
viewer.open(iiifServiceUrl + '/info.json')
|
| 51 |
+
} else {
|
| 52 |
+
// Image statique simple (pas de zoom tuilé)
|
| 53 |
+
viewer.open({ type: 'image', url: source })
|
| 54 |
+
}
|
| 55 |
|
|
|
|
| 56 |
viewer.addOnceHandler('open', () => {
|
| 57 |
onViewerReadyRef.current?.(viewer)
|
| 58 |
})
|
| 59 |
+
}, [source, iiifServiceUrl])
|
| 60 |
|
| 61 |
return (
|
| 62 |
<div className="relative w-full h-full bg-retro-black">
|
|
@@ -154,7 +154,8 @@ export default function Editor() {
|
|
| 154 |
)
|
| 155 |
}
|
| 156 |
|
| 157 |
-
const
|
|
|
|
| 158 |
const regions = master?.layout?.regions ?? []
|
| 159 |
|
| 160 |
return (
|
|
@@ -194,8 +195,8 @@ export default function Editor() {
|
|
| 194 |
className="flex-1 min-w-0"
|
| 195 |
>
|
| 196 |
<div className="relative w-full h-full">
|
| 197 |
-
<Viewer
|
| 198 |
-
{!
|
| 199 |
<div className="absolute inset-0 flex items-center justify-center bg-retro-gray text-retro-darkgray text-retro-sm">
|
| 200 |
Apercu non disponible
|
| 201 |
</div>
|
|
|
|
| 154 |
)
|
| 155 |
}
|
| 156 |
|
| 157 |
+
const iiifServiceUrl = master?.image?.iiif_service_url ?? null
|
| 158 |
+
const fallbackImageUrl = master?.image?.derivative_web ?? master?.image?.master ?? ''
|
| 159 |
const regions = master?.layout?.regions ?? []
|
| 160 |
|
| 161 |
return (
|
|
|
|
| 195 |
className="flex-1 min-w-0"
|
| 196 |
>
|
| 197 |
<div className="relative w-full h-full">
|
| 198 |
+
<Viewer iiifServiceUrl={iiifServiceUrl} fallbackImageUrl={fallbackImageUrl} onViewerReady={() => {}} />
|
| 199 |
+
{!iiifServiceUrl && !fallbackImageUrl && (
|
| 200 |
<div className="absolute inset-0 flex items-center justify-center bg-retro-gray text-retro-darkgray text-retro-sm">
|
| 201 |
Apercu non disponible
|
| 202 |
</div>
|
|
@@ -122,7 +122,8 @@ export default function Reader() {
|
|
| 122 |
}
|
| 123 |
|
| 124 |
const currentPage = pages[currentIndex]
|
| 125 |
-
const
|
|
|
|
| 126 |
const regions: Region[] = master?.layout?.regions ?? []
|
| 127 |
|
| 128 |
return (
|
|
@@ -168,12 +169,12 @@ export default function Reader() {
|
|
| 168 |
statusBar={
|
| 169 |
master
|
| 170 |
? `${master.editorial.status} — v${master.editorial.version}`
|
| 171 |
-
:
|
| 172 |
}
|
| 173 |
className="flex-[7] min-w-0"
|
| 174 |
>
|
| 175 |
<div className="relative w-full h-full">
|
| 176 |
-
<Viewer
|
| 177 |
<RegionOverlay
|
| 178 |
viewer={osdViewer}
|
| 179 |
regions={regions}
|
|
@@ -211,7 +212,7 @@ export default function Reader() {
|
|
| 211 |
)}
|
| 212 |
|
| 213 |
{/* Not analyzed / error badge */}
|
| 214 |
-
{!master && !loading &&
|
| 215 |
<div className="absolute top-2 left-2">
|
| 216 |
{masterError
|
| 217 |
? <RetroBadge variant="error">Erreur: {masterError}</RetroBadge>
|
|
@@ -261,7 +262,7 @@ export default function Reader() {
|
|
| 261 |
</div>
|
| 262 |
) : (
|
| 263 |
<div className="p-3 text-retro-sm text-retro-darkgray">
|
| 264 |
-
{
|
| 265 |
? 'Page non encore analysee par l\'IA.'
|
| 266 |
: 'Aucune image associee a cette page.'
|
| 267 |
}
|
|
|
|
| 122 |
}
|
| 123 |
|
| 124 |
const currentPage = pages[currentIndex]
|
| 125 |
+
const iiifServiceUrl = currentPage.iiif_service_url ?? null
|
| 126 |
+
const fallbackImageUrl = currentPage.image_master_path ?? ''
|
| 127 |
const regions: Region[] = master?.layout?.regions ?? []
|
| 128 |
|
| 129 |
return (
|
|
|
|
| 169 |
statusBar={
|
| 170 |
master
|
| 171 |
? `${master.editorial.status} — v${master.editorial.version}`
|
| 172 |
+
: (iiifServiceUrl || fallbackImageUrl) ? 'Page non analysee' : 'Aucune image'
|
| 173 |
}
|
| 174 |
className="flex-[7] min-w-0"
|
| 175 |
>
|
| 176 |
<div className="relative w-full h-full">
|
| 177 |
+
<Viewer iiifServiceUrl={iiifServiceUrl} fallbackImageUrl={fallbackImageUrl} onViewerReady={handleViewerReady} />
|
| 178 |
<RegionOverlay
|
| 179 |
viewer={osdViewer}
|
| 180 |
regions={regions}
|
|
|
|
| 212 |
)}
|
| 213 |
|
| 214 |
{/* Not analyzed / error badge */}
|
| 215 |
+
{!master && !loading && (iiifServiceUrl || fallbackImageUrl) && (
|
| 216 |
<div className="absolute top-2 left-2">
|
| 217 |
{masterError
|
| 218 |
? <RetroBadge variant="error">Erreur: {masterError}</RetroBadge>
|
|
|
|
| 262 |
</div>
|
| 263 |
) : (
|
| 264 |
<div className="p-3 text-retro-sm text-retro-darkgray">
|
| 265 |
+
{(iiifServiceUrl || fallbackImageUrl)
|
| 266 |
? 'Page non encore analysee par l\'IA.'
|
| 267 |
: 'Aucune image associee a cette page.'
|
| 268 |
}
|