I am using an open-source web-based viewer in a Vue3 app. The image is not shown until the user clicks a "Open Image" button. It works good.
However, does anyone know why the same image is making two network requests when the "Open Image" button is clicked?
Here's is my minimal reproduction:
sandbox: https://stackblitz.com/edit/vitejs-vite-xxxk9w?file=src/App.vue
App.vue
:
<script setup>
import { ref } from 'vue';
import Viewer from './components/Viewer.vue';
const show = ref(false);
</script>
<template>
<div>
<button type="button" @click="show = true">Open Image</button>
<Viewer v-if="show" />
</div>
</template>
Viewer.vue
:
<template>
<div ref="osdContainer" style="width: 500px; height: 500px"></div>
</template>
<script setup>
import OpenSeadragon from 'openseadragon';
import { ref, onMounted } from 'vue';
const viewer = ref(null);
const osdContainer = ref(null);
const initViewer = () => {
console.log('init Viewer');
viewer.value = OpenSeadragon({
element: osdContainer.value,
tileSources: {
type: 'image',
url: 'https://ik.imagekit.io/pixstery/users%2F5cnu6iDlTsa5mujH2sKPsBJ8OKH2%2Fposts%2Fportrait-of-arabian-man_jjC2?alt=media&token=64fb0ae4-b0dc-4ead-b22e-292e55de1447&tr=f-auto,pr-true,q-80',
buildPyramid: false,
},
});
};
onMounted(() => {
console.log('mounting..');
initViewer();
});
</script>
OpenSeadragon thinks in tiled image pyramids, where most of the time you access the image metadata (resolution, and the like) and the actual tiles (bitmap data) separately.
Supporting actual images is the outlier in such world, and it's still handled as if image metadata and bitmap data would arrive from separate sources.
The first request you see comes from getImageInfo()
of ImageTileSource
, the specialized class for supporting images:
var image = this._image = new Image();
[...]
$.addEvent(image, 'load', function () {
_this.width = image.naturalWidth;
_this.height = image.naturalHeight;
_this.aspectRatio = _this.width / _this.height;
_this.dimensions = new $.Point(_this.width, _this.height);
_this._tileWidth = _this.width;
_this._tileHeight = _this.height;
_this.tileOverlap = 0;
_this.minLevel = 0;
_this.levels = _this._buildLevels();
_this.maxLevel = _this.levels.length - 1;
_this.ready = true;
// Note: this event is documented elsewhere, in TileSource
_this.raiseEvent('ready', {tileSource: _this});
});
[...]
image.src = url; // <----------
and the second request is when the bitmap data is requested in _loadTile()
:
_loadTile: function(tile, time) {
var _this = this;
tile.loading = true;
this._imageLoader.addJob({
src: tile.getUrl(), // <-------
this part of the code is a generic one, TiledImage
, which is common for everything. And this is a weakness of current(*) OpenSeadragon: the generic code asks for an URL, and not for tile data. So it doesn't matter that the ImageTileSource
above stores the entire image in its _image
field (and even more (*)), the drawing code never asks for it, it wants and gets an URL, for which it issues a request.
getTileUrl()
of TileImageSource
indeed provides that URL without any magics:
var url = null;
if (level >= this.minLevel && level <= this.maxLevel) {
url = this.levels[level].url;
}
return url;
When mentioning "magic", I can think of usage of createObjectURL()
. Then you would download the image with fetch()
, ask for blob()
, do the createObjectURL()
, and use that URL for both the image.src =
line and return it in getTileUrl()
.
So if you have your own OpenSeadragon copy, it would become something like
getImageInfo: function (url) {
[...]
// image.src = url;
fetch(url)
.then(response => response.blob())
.then(blob => {
image.src = this.objurl = URL.createObjectURL(blob);
});
},
and
getTileUrl: function (level, x, y) {
return this.objurl;
},
(*) And why this probably doesn't matter:
levels[]
thing in the original getTileUrl()
(and _buildLevels()
at the end of the file). ImageTileSource
already stores the image and even an entire pyramid created from it, just it's not in use, yet.