Skip to content

support IIIF layout in dzsave #1465

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
jcupitt opened this issue Nov 8, 2019 · 22 comments
Closed

support IIIF layout in dzsave #1465

jcupitt opened this issue Nov 8, 2019 · 22 comments

Comments

@jcupitt
Copy link
Member

jcupitt commented Nov 8, 2019

dzsave currently supports google maps, zoomify and deepzoom tile layouts.

Another popular tile naming convention is IIIF, see:

go-iiif/go-iiif#20 (comment)

lovell/sharp#1335

@kjell do you have a tiny sample image in the IIIF format handy?

It looks like --layout=iiif would be google-like, but not pad edge tiles, and would name tiles differently.

@jcupitt
Copy link
Member Author

jcupitt commented Nov 8, 2019

Sample script by kjell to rename dzsave --layout google to iiif:

function getMaxZoom(imageSize, tileSize) {
  let { x, y } = imageSize

  return Math.ceil(Math.log(Math.max(x, y) / tileSize) / Math.LN2)
}

function zyxToIIIF(z, y, x, imageSize, tileSize = 512) {
  let { x: imageWidth, y: imageHeight } = imageSize
  let maxZoom = getMaxZoom(imageSize, tileSize)

  let scale = Math.pow(2, maxZoom - z)
  let tileBaseSize = tileSize * scale

  let minx = x * tileBaseSize,
    miny = y * tileBaseSize,
    maxx = Math.min(minx + tileBaseSize, imageWidth),
    maxy = Math.min(miny + tileBaseSize, imageHeight)

  let xDiff = maxx - minx
  let yDiff = maxy - miny
  let size = Math.ceil(xDiff / scale)

  let region = [minx, miny, xDiff, yDiff].join(',')
  let iiif = `/${region}/${size},/0/color.jpg`

  return iiif
}

module.exports = {
  zyxToIIIF,
  getMaxZoom,
}

if (!module.parent) {
  // If this is being run directly and not `require()`d…
  const fs = require('fs-extra')
  const sharp = require('sharp')

  function getNumericDirs(src) {
    return fs
      .readdirSync(src)
      .filter(dir => dir === '0' || dir === '0.jpg' || parseInt(dir))
      .sort((a, b) => a.replace('.jpg', '') - b.replace('.jpg', ''))
  }

  const src = process.argv[2]

  /* TODO
   * generate `info.json`
   * how to get image dimensions for an existing directory of tiles?
   * what to do when created files exist already: error?
   * split this into smaller files
   */

  function convertZyxTilesToIIIF(dir) {
    const imageSize = { x: 25616, y: 12341 } // TODO dont hardcode
    const zooms = getNumericDirs(dir)

    zooms.map((z, zoomIndex) => {
      getNumericDirs(`./${dir}/${z}`).map(y => {
        getNumericDirs(`./${dir}/${z}/${y}`).map(xFile => {
          const [x, ext] = xFile.split('.')
          const iiif = zyxToIIIF(z, y, x, imageSize)
          const _src = `${dir}/${z}/${y}/${x}.jpg`
          const dest = `${dir}-iiif${iiif}`

          fs.ensureDir(dest.replace('/color.jpg', ''), err => {
            err
              ? console.error(err)
              : fs.copy(_src, dest, err => {
                  err ? console.error(err) : console.info('.')
                })
          })
        })
      })
    })
  }

  function tileImageAndConvert(src) {
    const output = 'out'
    const image = sharp(src)
    const metadata = image
      .metadata()
      .then(meta => {
        const imageSize = { x: meta.width, y: meta.height }
        image
          .limitInputPixels(false)
          .tile({ layout: 'google', size: 512 })
          .toFile(output, (err, info) => {
            console.info('tiled', { err, info })
            convertZyxTilesToIIIF(output)
          })
      })
      .catch(err => console.error(err))
  }

  src.match(/.jpg/) ? tileImageAndConvert(src) : convertZyxTilesToIIIF(src)
}

@kjell
Copy link

kjell commented Nov 8, 2019

I am super interested in seeing this happen, but haven't gotten back to it since last year!

I have the exact opposite of a tiny image laying around: https://cdn.dx.artsmia.org/barbari-map-iiif-vips/index.html. Here's one that's a bit more manageable https://cdn.dx.artsmia.org/barbari-map-iiif-vips/5887.html

@kjell
Copy link

kjell commented Nov 8, 2019

The interesting thing about IIIF is that it provides a descriptive API to generate derivatives of a master image without needing to pre-render everything, so at a higher level it would be interesting to see how hard it is to build the full (or something more than "level 0", as the full API can get involved) IIIF spec into vips.

The IIIF "image api" uses https://iiif-compliant-image.server/:imageId/:region/:size/:rotation/:quality.jpg to specify a derived image. So …/full/full/0/native.jpg would return a full size jpg, but …/x0,y0,width,height/,500/90/native.tif should return a crop of that image from x0,y0 at the top left to x0+width,y0+height bottom right, that itself has been resized to max 500px tall (,500), rotated 90º, and in tif format if supported. More info in the docs

https://github.com/cmoa/iiif_s3 is a ruby library that can be looked to as a much more comprehensive example than my script above for building a "level 0" compliant IIIF resource. It amounts to calculating what region/size parameters are needed to create the pyramidal tiles then saving them out to disk.

Beyond implementing IIIF as an output format for generating tiles, implementing IIIF's notion of region/size/rotation/format more generally could allow a very lightweight image server to send image requests back and forth to vips to do the heavy lifting.

@atomotic
Copy link

atomotic commented Nov 8, 2019

other similar tools to look at:
https://github.com/cmu-lib/magick_tile
https://github.com/zimeon/iiif-static-book
https://github.com/edsilv/biiif

@edsu
Copy link

edsu commented Nov 8, 2019

However edsilv/biiif doesn't cut tiles, which is partly why it would be useful if libvips did!

@jcupitt
Copy link
Member Author

jcupitt commented Nov 8, 2019

Hi @kjell !

I saw your nice map example, but what does the precomputed pyramid look like? dzsave is just for making precomputed pyramids, so I'd need a sample pyramid to look at, I think.

Doing on-the-fly tiling is harder, of course. I did I tiny thing called tilesrv:

https://github.com/jcupitt/tilesrv

It can serve openslide images to deepzoom viewers and makes tiles and levels on the fly. It won't scale so well though -- more than a few 10s of clients on one server and it'll crawl.

Back in the 90s I did the first version of iipimage, and I think Ruven now supports iiif clients. But I don't think he uses libvips.

@kjell
Copy link

kjell commented Nov 9, 2019

It's pretty much what you said up top: "google-like, don't pad edge tiles, name tiles differently." The precomputed pyramid files will look something like this:

0,0,2048,2048/512,/0/default.jpg
0,2048,2048,2048/512,/0/default.jpg
2048,0,1083,2048/271,/0/default.jpg
2048,2048,1083,2048/271,/0/default.jpg
0,4096,2048,20/512,/0/default.jpg
2048,4096,1083,20/271,/0/default.jpg
2048,0,1024,1024/512,/0/default.jpg
0,3072,1024,1024/512,/0/default.jpg
2048,3072,1024,1024/512,/0/default.jpg
…

The tiles are still tileSize pixel square chunks of the master image going across and down the screen, just instead of using {z}/{y}/{x}.jpg or {z}/{x}_{y} they all start in the top level directory and are addressed by the IIIF :region/:size/:rotation/:quality.:format, with an image named default.jpg at the bottom.

dzsave --layout=iiif would also need to create an info.json metadata file at the top level. Different viewers expect slightly different things - a standard sized thumbnail might also enhance that.

@jcupitt
Copy link
Member Author

jcupitt commented Nov 9, 2019

Ah I think I see. Is the full/ directory required? That would be a little more work to create.

@jcupitt
Copy link
Member Author

jcupitt commented Nov 9, 2019

I has a quick go:

f499cef

On this laptop I see:

$ /usr/bin/time -f %M:%e vips dzsave ~/pics/wtc.jpg x --layout iiif --tile-size 256
108700:2.84

So 2.8s and a max of 110mb of ram for a 9300 x 9300 pixel jpg image. It creates info.json and all the tiles, but doesn't write the full/ directory.

I don't think I'm making info.json correctly -- I'm unclear about the rules for sizes and tiles. I'll try to think about this again.

jcupitt added a commit that referenced this issue Nov 11, 2019
better info.json, but still not support for the full/ directory

see #1465
@jcupitt
Copy link
Member Author

jcupitt commented Nov 11, 2019

I cleaned up the info.json a bit and I think it's all working. Would someone be able to test it?

There's still no full/ directory, I'm hoping this is optional.

Speed is in line with the other layouts. For a 10k x 10k pixel RGB JPG with the very nice magick_tile I see:

$ /usr/bin/time -f %M:%e python3 magick_tile.py -o x ~/pics/wtc.jpg
███████| 9/9 [00:22<00:00,  1.91s/it]
1450628:59.15

ie. 1.4gb of memory and 59s of time. With dzsave it's:

$ /usr/bin/time -f %M:%e vips dzsave wtc.jpg x --layout iiif
177496:0.94

ie. 180mb of memory and 0.94s.

@atomotic
Copy link

tried on macos , awesome 🎉
the result here with openseadragon https://tilde.club/~atomotic/vips_iiif/

i had to manually fix @id in info.json, this should be an optional argument in command line maybe

$ mediainfo img00152.jpg
General
Complete name                            : img00152.jpg
Format                                   : JPEG
File size                                : 6.53 MiB

Image
Format                                   : JPEG
Width                                    : 3 488 pixels
Height                                   : 5 232 pixels
Color space                              : YUV
Bit depth                                : 8 bits
Compression mode                         : Lossy
Stream size                              : 6.53 MiB (100%)


$ time vips dzsave img00152.jpg x --layout iiif --tile-size 256
vips dzsave img00152.jpg x --layout iiif --tile-size 256  2.23s user 1.04s system 181% cpu 1.798 total

@jcupitt
Copy link
Member Author

jcupitt commented Nov 11, 2019

It could put the "x" output directory name into the info.json, would that be enough?

@jcupitt
Copy link
Member Author

jcupitt commented Nov 11, 2019

(thanks for testing it, by the way!)

@jcupitt
Copy link
Member Author

jcupitt commented Nov 11, 2019

It now adds the basename to the json @id. This should be in libvips 8.9, due in a month or two. I'll close.

It supports a couple of other useful features. You can set the tile format like this:

vips dzsave img00152.jpg x --layout iiif --suffix .png

and it'll set the info.json as well as the tile format.

You can output directly to zips:

$ vips dzsave wtc.jpg x.zip --layout iiif
$ ls -l x.zip
-rw-r--r-- 1 john john 21349518 Nov 11 15:16 x.zip

And you can record all the image metadata:

$ vips dzsave wtc.jpg x --layout iiif --properties
$ more x/vips-properties.xml 
<?xml version="1.0"?>
<image xmlns="http://www.vips.ecs.soton.ac.uk//dzsave" date="2019-11-11T15:17:40
Z" version="8.9.0">
  <properties>
    <property>
      <name>width</name>
      <value type="gint">9372</value>
    </property>
... etc.

It can be handy to have a record of all image metadata for (for example) virtual slide images.

@atiro
Copy link

atiro commented Nov 18, 2019

Apologies late to this ticket, am very interested in making use of this feature, but it would really need the full region to be created for compliance with viewers (see https://iiif.io/api/image/3.0/compliance/#3-image-parameters). Good examples from Tom Crane to illustrate this here: https://tomcrane.github.io/scratch/osd/iiif-sizes.html

@jcupitt
Copy link
Member Author

jcupitt commented Nov 18, 2019

Hi @atiro, I opened an enhancement issue for this feature.

@edsilv
Copy link

edsilv commented Sep 1, 2022

However edsilv/biiif doesn't cut tiles, which is partly why it would be useful if libvips did!

@edsu It does now :-) https://github.com/IIIF-Commons/biiif-template

@AliFlux
Copy link

AliFlux commented Sep 15, 2022

Hi.

Is there any update/branch/PR on iiif support in libvips?

@jcupitt
Copy link
Member Author

jcupitt commented Sep 15, 2022

Yes, it was released three years ago in libvips 8.9 and has been improved a couple of times since. It supports IIIF3 now.

It's still missing the full/ stuff :( but that's simple to fix in a post-process step.

@peterrobinson
Copy link

tried on macos , awesome 🎉 the result here with openseadragon https://tilde.club/~atomotic/vips_iiif/

i had to manually fix @id in info.json, this should be an optional argument in command line maybe

$ mediainfo img00152.jpg
General
Complete name                            : img00152.jpg
Format                                   : JPEG
File size                                : 6.53 MiB

Image
Format                                   : JPEG
Width                                    : 3 488 pixels
Height                                   : 5 232 pixels
Color space                              : YUV
Bit depth                                : 8 bits
Compression mode                         : Lossy
Stream size                              : 6.53 MiB (100%)


$ time vips dzsave img00152.jpg x --layout iiif --tile-size 256
vips dzsave img00152.jpg x --layout iiif --tile-size 256  2.23s user 1.04s system 181% cpu 1.798 total

I too need to rewrite the id attribute in the info.json file. It would indeed be very neat if there were an option in the command line to set the id to the right place.

@jcupitt
Copy link
Member Author

jcupitt commented Feb 26, 2025

That's been supported since 8.10, @peterrobinson. Just set the id argument (if I understand what you need).

https://www.libvips.org/API/current/VipsForeignSave.html#vips-dzsave

@peterrobinson
Copy link

Yes, I discovered that after I wrote the message. Very helpful.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

8 participants