Overview

This article explains how to accurately display annotation coordinates (xywh format) from IIIF (International Image Interoperability Framework) Presentation API v3 manifests on a map viewer using Leaflet-IIIF.

This problem may seem simple at first glance, but accurate coordinate conversion is not possible without understanding the internal workings of Leaflet-IIIF.

Background

IIIF Manifest Annotation Format

In IIIF Presentation API v3, the target area of an annotation is specified in xywh format as follows:

{}""""}"itmb,tdyoo"""a"ptdtvlr:eiyyaag"v"plne":a:eugtht"eu"t"i{:"a:tAo:gpnn"e"sn"T""h::e:ttxt/a"t"petcu"jsxioa,a:aoml"/mnmB/p"eoel,ndxetya.i"mon,prglg"e/,.ioirigf//iciainfv/acsa/n1v/aasn/n1o#txaytwiho=n4/110"1,2,81,115,49"

This xywh=41012,81,115,49 means:

  • x: 41012 (left edge pixel position)
  • y: 81 (top edge pixel position)
  • w: 115 (width)
  • h: 49 (height)

These are pixel coordinates of the original image.

Leaflet-IIIF Coordinate System

Leaflet-IIIF is a Leaflet plugin that displays high-resolution images provided via the IIIF Image API in tile format. Internally it:

  1. Uses the CRS.Simple coordinate reference system
  2. Manages images at multiple zoom levels with reduced sizes
  3. Uses different coordinate scales for each zoom level

Due to this complex coordinate system, simply using map.unproject() or pointToLatLng() will not place annotations at the correct positions.

Trial and Error Process

Failed Attempt 1: Direct Use of map.unproject()

ccoonnssttploaitnLtng==L.mpaopi.nutn(pxr,ojye)c;t(point,3);3

Problem: unproject() assumes the current map coordinate system, which differs from the coordinate system used internally by Leaflet-IIIF.

Failed Attempt 2: Proportional Calculation from Map Bounds

cccccooooonnnnnssssstttttbnnllooonaurrgtnmmdXY==s==bb=ooiiuummmnnaggddpXYss...g/ggeeetiittBmmWNoaaeouggsrneettdWH(hsie)((di))tg;hh;t(;n(onromrXmY(b(obuonudnsd.sg.egteEtaNsotr(t)h(-)b-oubnodusn.dgse.tgWeetsSto(u)t)h)(;)));

Problem: Since Leaflet-IIIF preserves the image aspect ratio, the map bounds and the actual image bounds differ.

Failed Attempt 3: Bounds Calculation Considering Aspect Ratio

cci}}oofnness(ltticcsmoeimannmagss{apettgAAessavAppceseetrpccutettai調調clct=>Hael=mmiPaagaipphdmWAtdaisigdp=netegWhcmita=d/)ptW(hm{imada/ptpHhHieemi/iaggghihetmtH;aeg-iegAahsctpt;eucatl;Height)/2;

Problem: While this method can place annotations in approximately correct positions, it does not account for the reduced image sizes used internally by Leaflet-IIIF, resulting in slight misalignment.

The Correct Approach: Reproducing Leaflet-IIIF’s _fitBounds Method

Analyzing Leaflet-IIIF Source Code

Looking at Leaflet-IIIF’s _fitBounds method, we can see the following logic for placing images:

_}fitBvvvvvvv_oaaaaaaaturrrrrrrhnid_ioisnbsstnfmweo.:hifau_itsg==nmfsieedauatS__spn=litt.cZ=zhh=fttoeiiiiho_ssLtoimt=...Bnsh__lo(;=i_mmau)staatn_.hppLd{t_i..nshisoog(im.ppBbsa_ttoo.giiiuu_emoonngSannddeigsssstze..(,IeSccsnsirrwti.zss,rtle..uiesppnean[ooe)lgiii);Ztnnn;ohittotTTm-ioo(aLL_1laatZtth-oLLionns_mgg.t((_h+LLmi..asoppp.foo.ofiigpsnnetetttit((So]0iin;,mzsae.ig(mme)aaS)xgi;NezaSeti.izxve,e.Zy0o))o,,m;iinniittiiaallZZoooomm));;

Key points:

  1. _getInitialZoom() calculates the optimal zoom level
  2. _imageSizes[initialZoom + offset] gets the reduced image size at that zoom level
  3. That reduced size is used with pointToLatLng() for coordinate conversion

Correct Implementation

f}unctcccccccrioooooooennnnnnntn1s2s3s4ss5ss6u.t.t.t.tt.tt.rinmLioinnssLaenfmooccemgaifarraaaaeftsgmmllfpTlieeXYeel.oeatSddeoLtli0==XYtpa-Z=z-ttIoe1ii==iLIoimmonImi=ggnnngFiXYoos(=firr.iLi//mmLcmiaiXYergiyfiiasXieLiif.,fraiilpL.yffiieoia_eLLmmtimyiraaaa-ngem.yyggItYra_eeeeIT).girrSSIo_em..iiFL{gSaxyzzaeig;;eettze..LIeSxynnsi;;gi.z(tleLiesii.an[iiplgiiioZtnffiohiLLnotaatm-iyy((aeesm1lrrcaZ..ap-oxyl.oegim==deiXti+,SfiLoszafceyfa(esl)ree).td;o]Yp;)t,ioinnsi.tmiaaxlNZaotoimv)e;Zoom;

Complete Implementation Example

lla}deesottycnumict}}mairepifycn;fuatLn{ccccccm}ics}tc.acooooooa)ioe,coaytnnnnnnp;inthndeissssssIfsIT2sdrotttttt=cczILtIicccccf}a}0(oE;neroIaImoooooun)0elvmrmcpiLnsoFyaFennnnnnn;0reeiaeaaam.t:menossLsssco)r.nnnsnniame:rnuttettttccccrtccccccL}`;oetiipivngarLotaiooooeaoooooo.))rrLtfofatep:.0=t(iifioionnnnttnnnnnnr.;)ri(enesiS(Ca(mmlnfmnssssuisssssseaos)sssne'[RLt)aaeifattttrottttttccwffd<I{rttet=grm0S.iggttsginntoeiidsD(e{UAva,.to=ee-ieemnnssPtx[tbbalillTt:2'nr==mnipSin>WHIatSaooccmaayxooonogllorelanc'0ilPieIligrraaagrw,ptugrhCO(o$raanoe,]mea{diIZ=zemmllpeghLtnl:topmn{(=wwiI,pLgtgFoeTXYee..eyeode:laagi:'aaf=d{laehhoioddoit=,fms('ocp>n'Diieeytmi=L==XYpttRb#3ri)$d,Ottsc=,e==iate=twi=of,:t.{eMtar==fitii==ima,=gufybaxeC/fr.np.ciLiLmmosarhLn0':inrodeeivaiaiiiainggnnn.nghit.d0#nn+rnatstaiiniiiyfgXYoosfne]mls0f0doottcpesnivfiieL(rr.oota=a,0f.P.1reahom.tfaLffrai//mmcr..=gt'f5ob})n/(nsii(saLL.ymXYrEtseiL{,fpo;tmms[tni.yaa_egiisaapxTmn0udLaae0egmaeyyirXmm.crlyoag0pyonn.]mAanreem.,aaphgiwLgB'(.aiij;sngn.rra_ggiio(ethaeo,`vdffs[neox..giieemmi(t(.tTuaeeeo0oSt;y_emmWHaana;'sLonldssn].ea;gSagieggtn#pnLdu'tt(.brteigYdieeTnxlgase,_U)iovitze)tgSSooyi(t(}vr;tdioIeShhiiL,wtxLt<i3leycnnsi{;tzzah(,no/n_)m.esi.z;eeti='gpsic;ssI[tle..Ln',y(Ltto[ed0iesxynd)')xer)m0r]an[;;ge[);fo;p]v+;lgi(x1.+tna;iZtnL)]m,gccohi.;aw>te/otp=p,b<.[im-io>(obj0n(aiNytrs]fm1ln{ut>o.oaZtm+oni.p-o(bm'dj.osehR;;sgimcr)ioeia);gnti+l;h'Sfet)iLod).zafX;aeyf,d(esd)resT).tco;o]a(p;lmteaidpoY)n);s,.mianxiNtaitailvZeoZoomo)m;;

Why This Method Is Correct

Image Sizes at Each Zoom Level

Leaflet-IIIF manages images at multiple zoom levels for performance:

_]imagLLLLL4e.....3Sppppp8iooooo9ziiiii0ennnnnstttttx(((((=136143748338[236788,,,297,05136,500124823)))18,,,58)7,5)zzzzzoooooooooommmmm01238(maxNativeZoom)

The Role of initialZoom

_getInitialZoom() calculates the optimal zoom level for the map size:

_}getIvvf}rnaaoeirrrttuitoii}raofvmfnllfaaZesrg(2oreeiir;oatiSmmemniaat:c==zggueeeerfttSSnu=hh=iiniizzic0ssteet...h..-i8__ixyo;iisonmm.faa_f(ggittsmeemooeaSSalltpiigee;SzzerrieeSaazssinne..zcc)lleeeees{nn[<<ggitt]mmhh;aapp--SSii11zz;ee-..ixyt)h>&i=&{s.0o;ptii-o-n)s.{maxNativeZoom;

At this zoom level, the image is placed to fit neatly within the map.

The Meaning of Offset

offset adjusts the correspondence between the _imageSizes array index and the zoom level:

constoffset=iiifLayer._imageSizes.length-1-iiifLayer.options.maxNativeZoom;

For example, with maxNativeZoom = 8 and _imageSizes.length = 9, offset = 0.

Debugging Tips

If coordinate conversion is not working correctly, output the following information to the console for verification:

ccoonnssttploaitnLtng==L.mpaopi.nutn(pxr,ojye)c;t(point,3);3

0

Summary

To accurately display IIIF annotations in Leaflet-IIIF:

  1. Understand Leaflet-IIIF’s internal logic: How the _fitBounds method places the image
  2. Use the reduced image size: Use _imageSizes[initialZoom + offset] rather than the original image size
  3. Convert at the same zoom level: Convert with pointToLatLng(point, initialZoom)

This method allows annotations to be placed with pixel-level accuracy.

References

Project Information

  • Libraries used:
    • Leaflet 1.9.4
    • Leaflet-IIIF 3.0.0
  • Created: October 19, 2025