comparison ViewerPlugin/Plugin.cpp @ 256:7deea131c3c0 iiif

implementation of IIIF Image API
author Sebastien Jodogne <s.jodogne@gmail.com>
date Sat, 08 Jul 2023 18:44:54 +0200
parents 20a730889ae2
children 9af4ba0d92fe
comparison
equal deleted inserted replaced
255:cf20f8417eaa 256:7deea131c3c0
28 #include "OrthancPluginConnection.h" 28 #include "OrthancPluginConnection.h"
29 29
30 #include <Compatibility.h> // For std::unique_ptr 30 #include <Compatibility.h> // For std::unique_ptr
31 #include <Logging.h> 31 #include <Logging.h>
32 #include <Images/ImageProcessing.h> 32 #include <Images/ImageProcessing.h>
33 #include <Images/JpegReader.h>
34 #include <Images/JpegWriter.h>
33 #include <Images/PngWriter.h> 35 #include <Images/PngWriter.h>
34 #include <MultiThreading/Semaphore.h> 36 #include <MultiThreading/Semaphore.h>
35 #include <OrthancException.h> 37 #include <OrthancException.h>
36 #include <SystemToolbox.h> 38 #include <SystemToolbox.h>
37 39
38 #include "../Resources/Orthanc/Plugins/OrthancPluginCppWrapper.h" 40 #include "../Resources/Orthanc/Plugins/OrthancPluginCppWrapper.h"
39 41
40 #include <EmbeddedResources.h> 42 #include <EmbeddedResources.h>
41 43
42 #include <cassert> 44 #include <cassert>
45 #include <boost/regex.hpp>
46 #include <boost/math/special_functions/round.hpp>
43 47
44 std::unique_ptr<OrthancWSI::OrthancPluginConnection> orthanc_; 48 std::unique_ptr<OrthancWSI::OrthancPluginConnection> orthanc_;
45 std::unique_ptr<OrthancWSI::DicomPyramidCache> cache_; 49 std::unique_ptr<OrthancWSI::DicomPyramidCache> cache_;
46 std::unique_ptr<Orthanc::Semaphore> transcoderSemaphore_; 50 std::unique_ptr<Orthanc::Semaphore> transcoderSemaphore_;
47 51 static std::string publicIIIFUrl_;
48 52
49 static void AnswerSparseTile(OrthancPluginRestOutput* output, 53 static void AnswerSparseTile(OrthancPluginRestOutput* output,
50 unsigned int tileWidth, 54 unsigned int tileWidth,
51 unsigned int tileHeight) 55 unsigned int tileHeight)
52 { 56 {
132 std::string s = result.toStyledString(); 136 std::string s = result.toStyledString();
133 OrthancPluginAnswerBuffer(OrthancPlugins::GetGlobalContext(), output, s.c_str(), s.size(), "application/json"); 137 OrthancPluginAnswerBuffer(OrthancPlugins::GetGlobalContext(), output, s.c_str(), s.size(), "application/json");
134 } 138 }
135 139
136 140
141 class RawTile : public boost::noncopyable
142 {
143 private:
144 Orthanc::PixelFormat format_;
145 unsigned int tileWidth_;
146 unsigned int tileHeight_;
147 Orthanc::PhotometricInterpretation photometric_;
148 std::string tile_;
149 OrthancWSI::ImageCompression compression_;
150
151 Orthanc::ImageAccessor* DecodeInternal()
152 {
153 switch (compression_)
154 {
155 case OrthancWSI::ImageCompression_Jpeg:
156 {
157 std::unique_ptr<Orthanc::JpegReader> decoded(new Orthanc::JpegReader);
158 decoded->ReadFromMemory(tile_);
159 return decoded.release();
160 }
161
162 case OrthancWSI::ImageCompression_Jpeg2000:
163 {
164 std::unique_ptr<OrthancWSI::Jpeg2000Reader> decoded(new OrthancWSI::Jpeg2000Reader);
165 decoded->ReadFromMemory(tile_);
166
167 if (photometric_ == Orthanc::PhotometricInterpretation_YBR_ICT)
168 {
169 OrthancWSI::ImageToolbox::ConvertJpegYCbCrToRgb(*decoded);
170 }
171
172 return decoded.release();
173 }
174
175 case OrthancWSI::ImageCompression_None:
176 {
177 unsigned int bpp = Orthanc::GetBytesPerPixel(format_);
178 if (bpp * tileWidth_ * tileHeight_ != tile_.size())
179 {
180 throw Orthanc::OrthancException(Orthanc::ErrorCode_BadFileFormat);
181 }
182
183 std::unique_ptr<Orthanc::ImageAccessor> decoded(new Orthanc::ImageAccessor);
184 decoded->AssignReadOnly(format_, tileWidth_, tileHeight_, bpp * tileWidth_, tile_.c_str());
185
186 return decoded.release();
187 }
188
189 default:
190 throw Orthanc::OrthancException(Orthanc::ErrorCode_NotImplemented);
191 }
192 }
193
194 static void EncodeInternal(std::string& encoded,
195 const Orthanc::ImageAccessor& decoded,
196 Orthanc::MimeType transcodingType)
197 {
198 switch (transcodingType)
199 {
200 case Orthanc::MimeType_Png:
201 {
202 Orthanc::PngWriter writer;
203 Orthanc::IImageWriter::WriteToMemory(writer, encoded, decoded);
204 break;
205 }
206
207 case Orthanc::MimeType_Jpeg:
208 {
209 Orthanc::JpegWriter writer;
210 Orthanc::IImageWriter::WriteToMemory(writer, encoded, decoded);
211 break;
212 }
213
214 default:
215 throw Orthanc::OrthancException(Orthanc::ErrorCode_NotImplemented);
216 }
217 }
218
219
220 public:
221 RawTile(OrthancWSI::ITiledPyramid& pyramid,
222 unsigned int level,
223 unsigned int tileX,
224 unsigned int tileY) :
225 format_(pyramid.GetPixelFormat()),
226 tileWidth_(pyramid.GetTileWidth(level)),
227 tileHeight_(pyramid.GetTileHeight(level)),
228 photometric_(pyramid.GetPhotometricInterpretation())
229 {
230 if (!pyramid.ReadRawTile(tile_, compression_, level, tileX, tileY))
231 {
232 // Handling of missing tile (for sparse tiling): TODO parameter?
233 // AnswerSparseTile(output, tileWidth, tileHeight); return;
234 throw Orthanc::OrthancException(Orthanc::ErrorCode_UnknownResource);
235 }
236 }
237
238 void Answer(OrthancPluginRestOutput* output,
239 Orthanc::MimeType transcodingType)
240 {
241 if (compression_ == OrthancWSI::ImageCompression_Jpeg)
242 {
243 // The tile is already a JPEG image. In such a case, we can
244 // serve it as such, because any Web browser can handle JPEG.
245 OrthancPluginAnswerBuffer(OrthancPlugins::GetGlobalContext(), output, tile_.c_str(),
246 tile_.size(), Orthanc::EnumerationToString(Orthanc::MimeType_Jpeg));
247 }
248 else
249 {
250 // This is a lossless frame (coming from a JPEG2000 or an
251 // uncompressed DICOM instance), which is not a DICOM-JPEG
252 // instance. We need to decompress the raw tile, then transcode
253 // it to the PNG/JPEG, depending on the "transcodingType".
254
255 std::string transcoded;
256
257 {
258 // The semaphore is used to throttle the number of simultaneous computations
259 Orthanc::Semaphore::Locker locker(*transcoderSemaphore_);
260
261 std::unique_ptr<Orthanc::ImageAccessor> decoded(DecodeInternal());
262 EncodeInternal(transcoded, *decoded, transcodingType);
263 }
264
265 OrthancPluginAnswerBuffer(OrthancPlugins::GetGlobalContext(), output, transcoded.c_str(),
266 transcoded.size(), Orthanc::EnumerationToString(transcodingType));
267 }
268 }
269
270 Orthanc::ImageAccessor* Decode()
271 {
272 Orthanc::Semaphore::Locker locker(*transcoderSemaphore_);
273 return DecodeInternal();
274 }
275
276 static void Encode(std::string& encoded,
277 const Orthanc::ImageAccessor& decoded,
278 Orthanc::MimeType transcodingType)
279 {
280 Orthanc::Semaphore::Locker locker(*transcoderSemaphore_);
281 EncodeInternal(encoded, decoded, transcodingType);
282 }
283 };
284
285
137 void ServeTile(OrthancPluginRestOutput* output, 286 void ServeTile(OrthancPluginRestOutput* output,
138 const char* url, 287 const char* url,
139 const OrthancPluginHttpRequest* request) 288 const OrthancPluginHttpRequest* request)
140 { 289 {
141 std::string seriesId(request->groups[0]); 290 std::string seriesId(request->groups[0]);
153 { 302 {
154 throw Orthanc::OrthancException(Orthanc::ErrorCode_ParameterOutOfRange); 303 throw Orthanc::OrthancException(Orthanc::ErrorCode_ParameterOutOfRange);
155 } 304 }
156 305
157 // Retrieve the raw tile from the WSI pyramid 306 // Retrieve the raw tile from the WSI pyramid
158 OrthancWSI::ImageCompression compression; 307 std::unique_ptr<RawTile> rawTile;
159 Orthanc::PhotometricInterpretation photometric;
160 Orthanc::PixelFormat format;
161 std::string tile;
162 unsigned int tileWidth, tileHeight;
163 308
164 { 309 {
165 OrthancWSI::DicomPyramidCache::Locker locker(*cache_, seriesId); 310 OrthancWSI::DicomPyramidCache::Locker locker(*cache_, seriesId);
166 311 rawTile.reset(new RawTile(locker.GetPyramid(),
167 format = locker.GetPyramid().GetPixelFormat(); 312 static_cast<unsigned int>(level),
168 tileWidth = locker.GetPyramid().GetTileWidth(level); 313 static_cast<unsigned int>(tileX),
169 tileHeight = locker.GetPyramid().GetTileHeight(level); 314 static_cast<unsigned int>(tileY)));
170 photometric = locker.GetPyramid().GetPhotometricInterpretation(); 315 }
171 316
172 if (!locker.GetPyramid().ReadRawTile(tile, compression, 317 /**
173 static_cast<unsigned int>(level), 318 * In the case the DICOM file doesn't use the JPEG transfer syntax,
174 static_cast<unsigned int>(tileX), 319 * transfer the tile (which is presumably lossless) as a PNG image
175 static_cast<unsigned int>(tileY))) 320 * so as to prevent lossy compression. Don't call "rawTile" while
176 { 321 * the Locker is around, as "Answer()" can be a costly operation.
177 // Handling of missing tile (for sparse tiling): TODO parameter? 322 **/
178 // AnswerSparseTile(output, tileWidth, tileHeight); return; 323 rawTile->Answer(output, Orthanc::MimeType_Png);
179 throw Orthanc::OrthancException(Orthanc::ErrorCode_UnknownResource);
180 }
181 }
182
183
184 // Test whether the tile is a JPEG image. In such a case, we can
185 // serve it as such, because any Web browser can handle JPEG
186
187 if (compression == OrthancWSI::ImageCompression_Jpeg)
188 {
189 OrthancPluginAnswerBuffer(OrthancPlugins::GetGlobalContext(), output, tile.c_str(), tile.size(), "image/jpeg");
190 return; // We're done
191 }
192
193
194 // The tile does not come from a DICOM-JPEG instance, we need to
195 // decompress the raw tile
196 std::unique_ptr<Orthanc::ImageAccessor> decoded;
197
198 Orthanc::Semaphore::Locker locker(*transcoderSemaphore_);
199
200 switch (compression)
201 {
202 case OrthancWSI::ImageCompression_Jpeg2000:
203 decoded.reset(new OrthancWSI::Jpeg2000Reader);
204 dynamic_cast<OrthancWSI::Jpeg2000Reader&>(*decoded).ReadFromMemory(tile);
205
206 if (photometric == Orthanc::PhotometricInterpretation_YBR_ICT)
207 {
208 OrthancWSI::ImageToolbox::ConvertJpegYCbCrToRgb(*decoded);
209 }
210
211 break;
212
213 case OrthancWSI::ImageCompression_None:
214 {
215 unsigned int bpp = Orthanc::GetBytesPerPixel(format);
216 if (bpp * tileWidth * tileHeight != tile.size())
217 {
218 throw Orthanc::OrthancException(Orthanc::ErrorCode_BadFileFormat);
219 }
220
221 decoded.reset(new Orthanc::ImageAccessor);
222 decoded->AssignReadOnly(format, tileWidth, tileHeight, bpp * tileWidth, tile.c_str());
223 break;
224 }
225
226 default:
227 throw Orthanc::OrthancException(Orthanc::ErrorCode_NotImplemented);
228 }
229
230
231 // This is a lossless frame (coming from a JPEG2000 or uncompressed
232 // DICOM instance), serve it as a PNG image so as to prevent lossy
233 // compression
234
235 std::string png;
236 Orthanc::PngWriter writer;
237 Orthanc::IImageWriter::WriteToMemory(writer, png, *decoded);
238
239 OrthancPluginAnswerBuffer(OrthancPlugins::GetGlobalContext(), output, png.c_str(), png.size(), "image/png");
240 } 324 }
241 325
242 326
243 OrthancPluginErrorCode OnChangeCallback(OrthancPluginChangeType changeType, 327 OrthancPluginErrorCode OnChangeCallback(OrthancPluginChangeType changeType,
244 OrthancPluginResourceType resourceType, 328 OrthancPluginResourceType resourceType,
297 Orthanc::EmbeddedResources::GetFileResource(content, resource); 381 Orthanc::EmbeddedResources::GetFileResource(content, resource);
298 382
299 OrthancPluginAnswerBuffer(OrthancPlugins::GetGlobalContext(), output, content.c_str(), content.size(), mime.c_str()); 383 OrthancPluginAnswerBuffer(OrthancPlugins::GetGlobalContext(), output, content.c_str(), content.size(), mime.c_str());
300 } 384 }
301 385
386
387
388 void ServeIIIFImageInfo(OrthancPluginRestOutput* output,
389 const char* url,
390 const OrthancPluginHttpRequest* request)
391 {
392 std::string seriesId(request->groups[0]);
393
394 LOG(INFO) << "IIIF: Accessing whole-slide pyramid of series " << seriesId;
395
396 OrthancWSI::DicomPyramidCache::Locker locker(*cache_, seriesId);
397
398 if (locker.GetPyramid().GetLevelCount() == 0)
399 {
400 throw Orthanc::OrthancException(Orthanc::ErrorCode_InternalError);
401 }
402
403 if (locker.GetPyramid().GetTileWidth(0) != locker.GetPyramid().GetTileHeight(0))
404 {
405 throw Orthanc::OrthancException(Orthanc::ErrorCode_IncompatibleImageFormat,
406 "IIIF doesn't support non-isotropic tile sizes");
407 }
408
409 for (unsigned int i = 1; i < locker.GetPyramid().GetLevelCount(); i++)
410 {
411 if (locker.GetPyramid().GetTileWidth(i) != locker.GetPyramid().GetTileWidth(0) ||
412 locker.GetPyramid().GetTileHeight(i) != locker.GetPyramid().GetTileHeight(0))
413 {
414 throw Orthanc::OrthancException(Orthanc::ErrorCode_IncompatibleImageFormat,
415 "IIIF doesn't support levels with varying tile sizes");
416 }
417 }
418
419 Json::Value sizes = Json::arrayValue;
420 for (unsigned int i = locker.GetPyramid().GetLevelCount(); i > 0; i--)
421 {
422 Json::Value level;
423 level["width"] = locker.GetPyramid().GetLevelWidth(i - 1);
424 level["height"] = locker.GetPyramid().GetLevelHeight(i - 1);
425 sizes.append(level);
426 }
427
428 Json::Value scaleFactors = Json::arrayValue;
429 for (unsigned int i = locker.GetPyramid().GetLevelCount(); i > 0; i--)
430 {
431 scaleFactors.append(static_cast<float>(locker.GetPyramid().GetLevelWidth(0)) /
432 static_cast<float>(locker.GetPyramid().GetLevelWidth(i - 1)));
433 }
434
435 Json::Value tiles;
436 tiles["width"] = locker.GetPyramid().GetTileWidth(0);
437 tiles["height"] = locker.GetPyramid().GetTileHeight(0);
438 tiles["scaleFactors"] = scaleFactors;
439
440 Json::Value result;
441 result["@context"] = "http://iiif.io/api/image/2/context.json";
442 result["@id"] = publicIIIFUrl_ + seriesId;
443 result["profile"] = "http://iiif.io/api/image/2/level0.json";
444 result["protocol"] = "http://iiif.io/api/image";
445 result["width"] = locker.GetPyramid().GetLevelWidth(0);
446 result["height"] = locker.GetPyramid().GetLevelHeight(0);
447 result["sizes"] = sizes;
448
449 result["tiles"] = Json::arrayValue;
450 result["tiles"].append(tiles);
451
452 std::string s = result.toStyledString();
453 OrthancPluginAnswerBuffer(OrthancPlugins::GetGlobalContext(), output, s.c_str(), s.size(), "application/json");
454 }
455
456
457 static unsigned int GetPhysicalTileWidth(const OrthancWSI::ITiledPyramid& pyramid,
458 unsigned int level)
459 {
460 return static_cast<unsigned int>(boost::math::iround(
461 static_cast<float>(pyramid.GetTileWidth(level)) *
462 static_cast<float>(pyramid.GetLevelWidth(0)) /
463 static_cast<float>(pyramid.GetLevelWidth(level))));
464 }
465
466
467 static unsigned int GetPhysicalTileHeight(const OrthancWSI::ITiledPyramid& pyramid,
468 unsigned int level)
469 {
470 return static_cast<unsigned int>(boost::math::iround(
471 static_cast<float>(pyramid.GetTileHeight(level)) *
472 static_cast<float>(pyramid.GetLevelHeight(0)) /
473 static_cast<float>(pyramid.GetLevelHeight(level))));
474 }
475
476
477 void ServeIIIFImageTile(OrthancPluginRestOutput* output,
478 const char* url,
479 const OrthancPluginHttpRequest* request)
480 {
481 std::string seriesId(request->groups[0]);
482 std::string region(request->groups[1]);
483 std::string size(request->groups[2]);
484 std::string rotation(request->groups[3]);
485 std::string quality(request->groups[4]);
486 std::string format(request->groups[5]);
487
488 LOG(INFO) << "IIIF: Accessing tile of series " << seriesId << ": "
489 << "region=" << region << "; size=" << size << "; rotation="
490 << rotation << "; quality=" << quality << "; format=" << format;
491
492 if (region == "full")
493 {
494 throw Orthanc::OrthancException(Orthanc::ErrorCode_NotImplemented, "IIIF - Full region is not supported for whole-slide images");
495 }
496
497 if (rotation != "0")
498 {
499 throw Orthanc::OrthancException(Orthanc::ErrorCode_NotImplemented, "IIIF - Unsupported rotation: " + rotation);
500 }
501
502 if (quality != "default")
503 {
504 throw Orthanc::OrthancException(Orthanc::ErrorCode_NotImplemented, "IIIF - Unsupported quality: " + quality);
505 }
506
507 if (format != "jpg")
508 {
509 throw Orthanc::OrthancException(Orthanc::ErrorCode_NotImplemented, "IIIF - Unsupported format: " + format);
510 }
511
512 int regionX, regionY, regionWidth, regionHeight;
513
514 bool ok = false;
515 boost::regex regionPattern("([0-9]+),([0-9]+),([0-9]+),([0-9]+)");
516 boost::cmatch regionWhat;
517 if (regex_match(region.c_str(), regionWhat, regionPattern))
518 {
519 try
520 {
521 regionX = boost::lexical_cast<int>(regionWhat[1]);
522 regionY = boost::lexical_cast<int>(regionWhat[2]);
523 regionWidth = boost::lexical_cast<int>(regionWhat[3]);
524 regionHeight = boost::lexical_cast<int>(regionWhat[4]);
525 ok = (regionX >= 0 &&
526 regionY >= 0 &&
527 regionWidth > 0 &&
528 regionHeight > 0);
529 }
530 catch (boost::bad_lexical_cast&)
531 {
532 }
533 }
534
535 if (!ok)
536 {
537 throw Orthanc::OrthancException(Orthanc::ErrorCode_NotImplemented, "IIIF - Not a (x,y,width,height) region: " + region);
538 }
539
540 int cropWidth;
541 boost::regex sizePattern("([0-9]+),");
542 boost::cmatch sizeWhat;
543 if (regex_match(size.c_str(), sizeWhat, sizePattern))
544 {
545 try
546 {
547 cropWidth = boost::lexical_cast<int>(sizeWhat[1]);
548 ok = (cropWidth > 0);
549 }
550 catch (boost::bad_lexical_cast&)
551 {
552 }
553 }
554
555 if (!ok)
556 {
557 throw Orthanc::OrthancException(Orthanc::ErrorCode_NotImplemented, "IIIF - Not a (width,) size: " + size);
558 }
559
560 std::unique_ptr<RawTile> rawTile;
561 std::unique_ptr<Orthanc::ImageAccessor> toCrop;
562
563 {
564 OrthancWSI::DicomPyramidCache::Locker locker(*cache_, seriesId);
565
566 OrthancWSI::ITiledPyramid& pyramid = locker.GetPyramid();
567
568 unsigned int level;
569 for (level = 0; level < pyramid.GetLevelCount(); level++)
570 {
571 const unsigned int physicalTileWidth = GetPhysicalTileWidth(pyramid, level);
572 const unsigned int physicalTileHeight = GetPhysicalTileHeight(pyramid, level);
573
574 if (regionX % physicalTileWidth == 0 &&
575 regionY % physicalTileHeight == 0 &&
576 regionWidth <= physicalTileWidth &&
577 regionHeight <= physicalTileHeight)
578 {
579 break;
580 }
581 }
582
583 if (cropWidth > pyramid.GetTileWidth(level))
584 {
585 throw Orthanc::OrthancException(Orthanc::ErrorCode_BadRequest, "IIIF - Request for a cropping that is too large for the tile size");
586 }
587
588 if (level == pyramid.GetLevelCount())
589 {
590 throw Orthanc::OrthancException(Orthanc::ErrorCode_BadRequest, "IIIF - Cannot locate the level of interest");
591 }
592 else
593 {
594 rawTile.reset(new RawTile(locker.GetPyramid(), level,
595 regionX / GetPhysicalTileWidth(pyramid, level),
596 regionY / GetPhysicalTileHeight(pyramid, level)));
597
598 if (cropWidth < pyramid.GetTileWidth(level))
599 {
600 toCrop.reset(rawTile->Decode());
601 rawTile.reset(NULL);
602 }
603 }
604 }
605
606 if (rawTile.get() != NULL)
607 {
608 assert(toCrop.get() == NULL);
609
610 // Level 0 Compliance of IIIF expects JPEG files
611 rawTile->Answer(output, Orthanc::MimeType_Jpeg);
612 }
613 else if (toCrop.get() != NULL)
614 {
615 assert(rawTile.get() == NULL);
616 assert(cropWidth < toCrop->GetWidth());
617
618 Orthanc::ImageAccessor cropped;
619 toCrop->GetRegion(cropped, 0, 0, cropWidth, toCrop->GetHeight());
620
621 std::string encoded;
622 RawTile::Encode(encoded, cropped, Orthanc::MimeType_Jpeg);
623
624 OrthancPluginAnswerBuffer(OrthancPlugins::GetGlobalContext(), output, encoded.c_str(),
625 encoded.size(), Orthanc::EnumerationToString(Orthanc::MimeType_Jpeg));
626 }
627 else
628 {
629 throw Orthanc::OrthancException(Orthanc::ErrorCode_InternalError);
630 }
631 }
302 632
303 633
304 extern "C" 634 extern "C"
305 { 635 {
306 ORTHANC_PLUGINS_API int32_t OrthancPluginInitialize(OrthancPluginContext* context) 636 ORTHANC_PLUGINS_API int32_t OrthancPluginInitialize(OrthancPluginContext* context)
346 OrthancPluginSetDescription(context, "Provides a Web viewer of whole-slide microscopic images within Orthanc."); 676 OrthancPluginSetDescription(context, "Provides a Web viewer of whole-slide microscopic images within Orthanc.");
347 677
348 orthanc_.reset(new OrthancWSI::OrthancPluginConnection); 678 orthanc_.reset(new OrthancWSI::OrthancPluginConnection);
349 cache_.reset(new OrthancWSI::DicomPyramidCache(*orthanc_, 10 /* Number of pyramids to be cached - TODO parameter */)); 679 cache_.reset(new OrthancWSI::DicomPyramidCache(*orthanc_, 10 /* Number of pyramids to be cached - TODO parameter */));
350 680
681 {
682 // TODO => CONFIG
683 publicIIIFUrl_ = "http://localhost:8042/wsi/iiif";
684
685 if (publicIIIFUrl_.empty() ||
686 publicIIIFUrl_[publicIIIFUrl_.size() - 1] != '/')
687 {
688 publicIIIFUrl_ += "/";
689 }
690 }
691
351 OrthancPluginRegisterOnChangeCallback(OrthancPlugins::GetGlobalContext(), OnChangeCallback); 692 OrthancPluginRegisterOnChangeCallback(OrthancPlugins::GetGlobalContext(), OnChangeCallback);
352 693
353 OrthancPlugins::RegisterRestCallback<ServeFile>("/wsi/app/(ol.css)", true); 694 OrthancPlugins::RegisterRestCallback<ServeFile>("/wsi/app/(ol.css)", true);
354 OrthancPlugins::RegisterRestCallback<ServeFile>("/wsi/app/(ol.js)", true); 695 OrthancPlugins::RegisterRestCallback<ServeFile>("/wsi/app/(ol.js)", true);
355 OrthancPlugins::RegisterRestCallback<ServeFile>("/wsi/app/(viewer.html)", true); 696 OrthancPlugins::RegisterRestCallback<ServeFile>("/wsi/app/(viewer.html)", true);
356 OrthancPlugins::RegisterRestCallback<ServeFile>("/wsi/app/(viewer.js)", true); 697 OrthancPlugins::RegisterRestCallback<ServeFile>("/wsi/app/(viewer.js)", true);
357 OrthancPlugins::RegisterRestCallback<ServePyramid>("/wsi/pyramids/([0-9a-f-]+)", true); 698 OrthancPlugins::RegisterRestCallback<ServePyramid>("/wsi/pyramids/([0-9a-f-]+)", true);
358 OrthancPlugins::RegisterRestCallback<ServeTile>("/wsi/tiles/([0-9a-f-]+)/([0-9-]+)/([0-9-]+)/([0-9-]+)", true); 699 OrthancPlugins::RegisterRestCallback<ServeTile>("/wsi/tiles/([0-9a-f-]+)/([0-9-]+)/([0-9-]+)/([0-9-]+)", true);
359 700
701 OrthancPlugins::RegisterRestCallback<ServeIIIFImageInfo>("/wsi/iiif/([0-9a-f-]+)/info.json", true);
702 OrthancPlugins::RegisterRestCallback<ServeIIIFImageTile>("/wsi/iiif/([0-9a-f-]+)/([0-9a-z,:]+)/([0-9a-z,!:]+)/([0-9,!]+)/([a-z]+)\\.([a-z]+)", true);
703
360 // Extend the default Orthanc Explorer with custom JavaScript for WSI 704 // Extend the default Orthanc Explorer with custom JavaScript for WSI
361 std::string explorer; 705 std::string explorer;
362 Orthanc::EmbeddedResources::GetFileResource(explorer, Orthanc::EmbeddedResources::ORTHANC_EXPLORER); 706 Orthanc::EmbeddedResources::GetFileResource(explorer, Orthanc::EmbeddedResources::ORTHANC_EXPLORER);
363 OrthancPluginExtendOrthancExplorer(OrthancPlugins::GetGlobalContext(), explorer.c_str()); 707 OrthancPluginExtendOrthancExplorer(OrthancPlugins::GetGlobalContext(), explorer.c_str());
364 708