Skip to content

Commit 12d35bb

Browse files
committed
examples/deepzoom: Add deepzoom_multiserver.py
Based on https://github.com/bgilbert/slidedeck commit 2b7bb287.
1 parent b5b8bbe commit 12d35bb

File tree

3 files changed

+284
-0
lines changed

3 files changed

+284
-0
lines changed
Lines changed: 198 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,198 @@
1+
#!/usr/bin/env python
2+
#
3+
# deepzoom_multiserver - Example web application for viewing multiple slides
4+
#
5+
# Copyright (c) 2010-2014 Carnegie Mellon University
6+
#
7+
# This library is free software; you can redistribute it and/or modify it
8+
# under the terms of version 2.1 of the GNU Lesser General Public License
9+
# as published by the Free Software Foundation.
10+
#
11+
# This library is distributed in the hope that it will be useful, but
12+
# WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
13+
# or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public
14+
# License for more details.
15+
#
16+
# You should have received a copy of the GNU Lesser General Public License
17+
# along with this library; if not, write to the Free Software Foundation,
18+
# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
19+
#
20+
21+
from collections import OrderedDict
22+
from flask import Flask, abort, make_response, render_template, url_for
23+
from io import BytesIO
24+
from openslide import OpenSlide, OpenSlideError
25+
from openslide.deepzoom import DeepZoomGenerator
26+
import os
27+
from optparse import OptionParser
28+
from threading import Lock
29+
30+
SLIDE_DIR = '.'
31+
SLIDE_CACHE_SIZE = 10
32+
DEEPZOOM_FORMAT = 'jpeg'
33+
DEEPZOOM_TILE_SIZE = 256
34+
DEEPZOOM_OVERLAP = 1
35+
DEEPZOOM_LIMIT_BOUNDS = True
36+
DEEPZOOM_TILE_QUALITY = 75
37+
38+
app = Flask(__name__)
39+
app.config.from_object(__name__)
40+
app.config.from_envvar('DEEPZOOM_MULTISERVER_SETTINGS', silent=True)
41+
42+
43+
class _SlideCache(object):
44+
def __init__(self, cache_size, dz_opts):
45+
self.cache_size = cache_size
46+
self.dz_opts = dz_opts
47+
self._lock = Lock()
48+
self._cache = OrderedDict()
49+
50+
def get(self, path):
51+
with self._lock:
52+
if path in self._cache:
53+
# Move to end of LRU
54+
slide = self._cache.pop(path)
55+
self._cache[path] = slide
56+
return slide
57+
slide = DeepZoomGenerator(OpenSlide(path), **self.dz_opts)
58+
with self._lock:
59+
if path not in self._cache:
60+
if len(self._cache) == self.cache_size:
61+
self._cache.popitem(last=False)
62+
self._cache[path] = slide
63+
return slide
64+
65+
66+
class _Directory(object):
67+
def __init__(self, basedir, relpath=''):
68+
self.name = os.path.basename(relpath)
69+
self.children = []
70+
for name in sorted(os.listdir(os.path.join(basedir, relpath))):
71+
cur_relpath = os.path.join(relpath, name)
72+
cur_path = os.path.join(basedir, cur_relpath)
73+
if os.path.isdir(cur_path):
74+
cur_dir = _Directory(basedir, cur_relpath)
75+
if cur_dir.children:
76+
self.children.append(cur_dir)
77+
elif OpenSlide.detect_format(cur_path):
78+
self.children.append(_SlideFile(cur_relpath))
79+
80+
81+
class _SlideFile(object):
82+
def __init__(self, relpath):
83+
self.name = os.path.basename(relpath)
84+
self.url_path = relpath
85+
86+
87+
@app.before_first_request
88+
def _setup():
89+
app.basedir = os.path.abspath(app.config['SLIDE_DIR'])
90+
config_map = {
91+
'DEEPZOOM_TILE_SIZE': 'tile_size',
92+
'DEEPZOOM_OVERLAP': 'overlap',
93+
'DEEPZOOM_LIMIT_BOUNDS': 'limit_bounds',
94+
}
95+
opts = dict((v, app.config[k]) for k, v in config_map.items())
96+
app.cache = _SlideCache(app.config['SLIDE_CACHE_SIZE'], opts)
97+
98+
99+
def _get_slide(path):
100+
path = os.path.abspath(os.path.join(app.basedir, path))
101+
if not path.startswith(app.basedir + os.path.sep):
102+
# Directory traversal
103+
abort(404)
104+
if not os.path.exists(path):
105+
abort(404)
106+
try:
107+
slide = app.cache.get(path)
108+
slide.filename = os.path.basename(path)
109+
return slide
110+
except OpenSlideError:
111+
abort(404)
112+
113+
114+
@app.route('/')
115+
def index():
116+
return render_template('files.html', root_dir=_Directory(app.basedir))
117+
118+
119+
@app.route('/<path:path>')
120+
def slide(path):
121+
slide = _get_slide(path)
122+
slide_url = url_for('dzi', path=path)
123+
return render_template('slide-fullpage.html', slide_url=slide_url,
124+
slide_filename=slide.filename)
125+
126+
127+
@app.route('/<path:path>.dzi')
128+
def dzi(path):
129+
slide = _get_slide(path)
130+
format = app.config['DEEPZOOM_FORMAT']
131+
resp = make_response(slide.get_dzi(format))
132+
resp.mimetype = 'application/xml'
133+
return resp
134+
135+
136+
@app.route('/<path:path>_files/<int:level>/<int:col>_<int:row>.<format>')
137+
def tile(path, level, col, row, format):
138+
slide = _get_slide(path)
139+
format = format.lower()
140+
if format != 'jpeg' and format != 'png':
141+
# Not supported by Deep Zoom
142+
abort(404)
143+
try:
144+
tile = slide.get_tile(level, (col, row))
145+
except ValueError:
146+
# Invalid level or coordinates
147+
abort(404)
148+
buf = BytesIO()
149+
tile.save(buf, format, quality=app.config['DEEPZOOM_TILE_QUALITY'])
150+
resp = make_response(buf.getvalue())
151+
resp.mimetype = 'image/%s' % format
152+
return resp
153+
154+
155+
if __name__ == '__main__':
156+
parser = OptionParser(usage='Usage: %prog [options] [slide-directory]')
157+
parser.add_option('-B', '--ignore-bounds', dest='DEEPZOOM_LIMIT_BOUNDS',
158+
default=True, action='store_false',
159+
help='display entire scan area')
160+
parser.add_option('-c', '--config', metavar='FILE', dest='config',
161+
help='config file')
162+
parser.add_option('-d', '--debug', dest='DEBUG', action='store_true',
163+
help='run in debugging mode (insecure)')
164+
parser.add_option('-e', '--overlap', metavar='PIXELS',
165+
dest='DEEPZOOM_OVERLAP', type='int',
166+
help='overlap of adjacent tiles [1]')
167+
parser.add_option('-f', '--format', metavar='{jpeg|png}',
168+
dest='DEEPZOOM_FORMAT',
169+
help='image format for tiles [jpeg]')
170+
parser.add_option('-l', '--listen', metavar='ADDRESS', dest='host',
171+
default='127.0.0.1',
172+
help='address to listen on [127.0.0.1]')
173+
parser.add_option('-p', '--port', metavar='PORT', dest='port',
174+
type='int', default=5000,
175+
help='port to listen on [5000]')
176+
parser.add_option('-Q', '--quality', metavar='QUALITY',
177+
dest='DEEPZOOM_TILE_QUALITY', type='int',
178+
help='JPEG compression quality [75]')
179+
parser.add_option('-s', '--size', metavar='PIXELS',
180+
dest='DEEPZOOM_TILE_SIZE', type='int',
181+
help='tile size [256]')
182+
183+
(opts, args) = parser.parse_args()
184+
# Load config file if specified
185+
if opts.config is not None:
186+
app.config.from_pyfile(opts.config)
187+
# Overwrite only those settings specified on the command line
188+
for k in dir(opts):
189+
if not k.startswith('_') and getattr(opts, k) is None:
190+
delattr(opts, k)
191+
app.config.from_object(opts)
192+
# Set slide directory
193+
try:
194+
app.config['SLIDE_DIR'] = args[0]
195+
except IndexError:
196+
pass
197+
198+
app.run(host=opts.host, port=opts.port, threaded=True)
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
<!doctype html>
2+
<title>Available Slides</title>
3+
4+
<style type="text/css">
5+
li {
6+
list-style-type: none;
7+
margin: 0.4em 0;
8+
}
9+
li.none {
10+
font-style: italic;
11+
}
12+
</style>
13+
14+
<h1>Available Slides</h1>
15+
16+
<ul>
17+
{% for entry in root_dir.children recursive %}
18+
<li>
19+
{% if entry.url_path %}
20+
<a href="{{ url_for('slide', path=entry.url_path) }}">
21+
{{ entry.name }}
22+
</a>
23+
{% else %}
24+
{{ entry.name }}
25+
{% endif %}
26+
27+
{% if entry.children %}
28+
<ul>
29+
{{ loop(entry.children) }}
30+
</ul>
31+
{% endif %}
32+
</li>
33+
{% else %}
34+
<li class="none">None</li>
35+
{% endfor %}
36+
</ul>
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
<!doctype html>
2+
<title>{{ slide_filename }}</title>
3+
4+
<style type="text/css">
5+
html {
6+
overflow: hidden;
7+
}
8+
body {
9+
margin: 0;
10+
padding: 0;
11+
}
12+
div#view {
13+
position: absolute;
14+
left: 0;
15+
width: 100%;
16+
height: 100%;
17+
background-color: black;
18+
color: white;
19+
}
20+
</style>
21+
22+
<div id="view"></div>
23+
24+
<script type="text/javascript" src="{{ url_for('static', filename='jquery.js') }}"></script>
25+
<script type="text/javascript" src="{{ url_for('static', filename='openseadragon.js') }}"></script>
26+
<script type="text/javascript">
27+
$(document).ready(function() {
28+
var viewer = new OpenSeadragon({
29+
id: "view",
30+
tileSources: "{{ slide_url }}",
31+
prefixUrl: "{{ url_for('static', filename='images/') }}",
32+
showNavigator: true,
33+
animationTime: 0.5,
34+
blendTime: 0.1,
35+
constrainDuringPan: true,
36+
maxZoomPixelRatio: 2,
37+
minZoomLevel: 1,
38+
visibilityRatio: 1,
39+
zoomPerScroll: 2,
40+
timeout: 120000,
41+
});
42+
viewer.addHandler("open", function() {
43+
// To improve load times, ignore the lowest-resolution Deep Zoom
44+
// levels. This is a hack: we can't configure the minLevel via
45+
// OpenSeadragon configuration options when the viewer is created
46+
// from DZI XML.
47+
viewer.source.minLevel = 8;
48+
});
49+
});
50+
</script>

0 commit comments

Comments
 (0)