Claude commited on
Commit
6736e17
·
unverified ·
1 Parent(s): 9b4e099

feat: IIIF-native Sprints 4-5-6 — tiled zoom, exports, cleanup

Browse files

Sprint 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

CLAUDE.md CHANGED
@@ -159,9 +159,12 @@ iiif-studio/
159
  ├── data/ ← JAMAIS versionné (.gitignore)
160
  │ └── corpora/
161
  │ └── {corpus_slug}/
162
- │ ├── masters/ ← images sources originales
163
- │ ├── derivatives/ ← JPEG 1500px pour l'IA
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/
backend/app/services/export/alto.py CHANGED
@@ -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:
backend/app/services/export/iiif.py CHANGED
@@ -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
- "id": image_url,
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
  }
backend/app/services/export/mets.py CHANGED
@@ -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": page.image.master or "",
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
- "LOCTYPE": "OTHER",
193
- "OTHERLOCTYPE": "filepath",
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
 
frontend/src/components/Viewer.tsx CHANGED
@@ -3,14 +3,16 @@ import OpenSeadragon from 'openseadragon'
3
  import { RetroButton } from './retro'
4
 
5
  interface Props {
6
- imageUrl: string
 
 
 
7
  onViewerReady?: (viewer: OpenSeadragon.Viewer) => void
8
  }
9
 
10
- const Viewer: FC<Props> = ({ imageUrl, onViewerReady }) => {
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 || !imageUrl) return
 
 
 
 
 
 
 
 
41
 
42
- viewer.open({ type: 'image', url: imageUrl })
43
  viewer.addOnceHandler('open', () => {
44
  onViewerReadyRef.current?.(viewer)
45
  })
46
- }, [imageUrl])
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">
frontend/src/pages/Editor.tsx CHANGED
@@ -154,7 +154,8 @@ export default function Editor() {
154
  )
155
  }
156
 
157
- const imageUrl = master?.image?.derivative_web ?? master?.image?.master ?? ''
 
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 imageUrl={imageUrl} onViewerReady={() => {}} />
198
- {!imageUrl && (
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>
frontend/src/pages/Reader.tsx CHANGED
@@ -122,7 +122,8 @@ export default function Reader() {
122
  }
123
 
124
  const currentPage = pages[currentIndex]
125
- const imageUrl = currentPage.image_master_path ?? ''
 
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
- : imageUrl ? 'Page non analysee' : 'Aucune image'
172
  }
173
  className="flex-[7] min-w-0"
174
  >
175
  <div className="relative w-full h-full">
176
- <Viewer imageUrl={imageUrl} onViewerReady={handleViewerReady} />
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 && imageUrl && (
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
- {imageUrl
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
  }