|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
from io import BytesIO |
|
from optparse import OptionParser |
|
import os |
|
import re |
|
from unicodedata import normalize |
|
|
|
from flask import Flask, abort, make_response, render_template, url_for |
|
|
|
if os.name == 'nt': |
|
_dll_path = os.getenv('OPENSLIDE_PATH') |
|
if _dll_path is not None: |
|
if hasattr(os, 'add_dll_directory'): |
|
|
|
with os.add_dll_directory(_dll_path): |
|
import openslide |
|
else: |
|
|
|
_orig_path = os.environ.get('PATH', '') |
|
os.environ['PATH'] = _orig_path + ';' + _dll_path |
|
import openslide |
|
|
|
os.environ['PATH'] = _orig_path |
|
else: |
|
import openslide |
|
|
|
from openslide import ImageSlide, open_slide |
|
from openslide.deepzoom import DeepZoomGenerator |
|
|
|
DEEPZOOM_SLIDE = None |
|
DEEPZOOM_FORMAT = 'jpeg' |
|
DEEPZOOM_TILE_SIZE = 254 |
|
DEEPZOOM_OVERLAP = 1 |
|
DEEPZOOM_LIMIT_BOUNDS = True |
|
DEEPZOOM_TILE_QUALITY = 75 |
|
SLIDE_NAME = 'slide' |
|
|
|
app = Flask(__name__) |
|
app.config.from_object(__name__) |
|
app.config.from_envvar('DEEPZOOM_TILER_SETTINGS', silent=True) |
|
|
|
|
|
@app.before_first_request |
|
def load_slide(): |
|
slidefile = app.config['DEEPZOOM_SLIDE'] |
|
if slidefile is None: |
|
raise ValueError('No slide file specified') |
|
config_map = { |
|
'DEEPZOOM_TILE_SIZE': 'tile_size', |
|
'DEEPZOOM_OVERLAP': 'overlap', |
|
'DEEPZOOM_LIMIT_BOUNDS': 'limit_bounds', |
|
} |
|
opts = {v: app.config[k] for k, v in config_map.items()} |
|
slide = open_slide(slidefile) |
|
app.slides = {SLIDE_NAME: DeepZoomGenerator(slide, **opts)} |
|
app.associated_images = [] |
|
app.slide_properties = slide.properties |
|
for name, image in slide.associated_images.items(): |
|
app.associated_images.append(name) |
|
slug = slugify(name) |
|
app.slides[slug] = DeepZoomGenerator(ImageSlide(image), **opts) |
|
try: |
|
mpp_x = slide.properties[openslide.PROPERTY_NAME_MPP_X] |
|
mpp_y = slide.properties[openslide.PROPERTY_NAME_MPP_Y] |
|
app.slide_mpp = (float(mpp_x) + float(mpp_y)) / 2 |
|
except (KeyError, ValueError): |
|
app.slide_mpp = 0 |
|
|
|
|
|
@app.route('/') |
|
def index(): |
|
slide_url = url_for('dzi', slug=SLIDE_NAME) |
|
associated_urls = { |
|
name: url_for('dzi', slug=slugify(name)) for name in app.associated_images |
|
} |
|
return render_template( |
|
'slide-multipane.html', |
|
slide_url=slide_url, |
|
associated=associated_urls, |
|
properties=app.slide_properties, |
|
slide_mpp=app.slide_mpp, |
|
) |
|
|
|
|
|
@app.route('/<slug>.dzi') |
|
def dzi(slug): |
|
format = app.config['DEEPZOOM_FORMAT'] |
|
try: |
|
resp = make_response(app.slides[slug].get_dzi(format)) |
|
resp.mimetype = 'application/xml' |
|
return resp |
|
except KeyError: |
|
|
|
abort(404) |
|
|
|
|
|
@app.route('/<slug>_files/<int:level>/<int:col>_<int:row>.<format>') |
|
def tile(slug, level, col, row, format): |
|
format = format.lower() |
|
if format != 'jpeg' and format != 'png': |
|
|
|
abort(404) |
|
try: |
|
tile = app.slides[slug].get_tile(level, (col, row)) |
|
except KeyError: |
|
|
|
abort(404) |
|
except ValueError: |
|
|
|
abort(404) |
|
buf = BytesIO() |
|
tile.save(buf, format, quality=app.config['DEEPZOOM_TILE_QUALITY']) |
|
resp = make_response(buf.getvalue()) |
|
resp.mimetype = 'image/%s' % format |
|
return resp |
|
|
|
|
|
def slugify(text): |
|
text = normalize('NFKD', text.lower()).encode('ascii', 'ignore').decode() |
|
return re.sub('[^a-z0-9]+', '-', text) |
|
|
|
|
|
if __name__ == '__main__': |
|
parser = OptionParser(usage='Usage: %prog [options] [slide]') |
|
parser.add_option( |
|
'-B', |
|
'--ignore-bounds', |
|
dest='DEEPZOOM_LIMIT_BOUNDS', |
|
default=True, |
|
action='store_false', |
|
help='display entire scan area', |
|
) |
|
parser.add_option( |
|
'-c', '--config', metavar='FILE', dest='config', help='config file' |
|
) |
|
parser.add_option( |
|
'-d', |
|
'--debug', |
|
dest='DEBUG', |
|
action='store_true', |
|
help='run in debugging mode (insecure)', |
|
) |
|
parser.add_option( |
|
'-e', |
|
'--overlap', |
|
metavar='PIXELS', |
|
dest='DEEPZOOM_OVERLAP', |
|
type='int', |
|
help='overlap of adjacent tiles [1]', |
|
) |
|
parser.add_option( |
|
'-f', |
|
'--format', |
|
metavar='{jpeg|png}', |
|
dest='DEEPZOOM_FORMAT', |
|
help='image format for tiles [jpeg]', |
|
) |
|
parser.add_option( |
|
'-l', |
|
'--listen', |
|
metavar='ADDRESS', |
|
dest='host', |
|
default='127.0.0.1', |
|
help='address to listen on [127.0.0.1]', |
|
) |
|
parser.add_option( |
|
'-p', |
|
'--port', |
|
metavar='PORT', |
|
dest='port', |
|
type='int', |
|
default=5000, |
|
help='port to listen on [5000]', |
|
) |
|
parser.add_option( |
|
'-Q', |
|
'--quality', |
|
metavar='QUALITY', |
|
dest='DEEPZOOM_TILE_QUALITY', |
|
type='int', |
|
help='JPEG compression quality [75]', |
|
) |
|
parser.add_option( |
|
'-s', |
|
'--size', |
|
metavar='PIXELS', |
|
dest='DEEPZOOM_TILE_SIZE', |
|
type='int', |
|
help='tile size [254]', |
|
) |
|
|
|
(opts, args) = parser.parse_args() |
|
|
|
if opts.config is not None: |
|
app.config.from_pyfile(opts.config) |
|
|
|
for k in dir(opts): |
|
if not k.startswith('_') and getattr(opts, k) is None: |
|
delattr(opts, k) |
|
app.config.from_object(opts) |
|
|
|
try: |
|
app.config['DEEPZOOM_SLIDE'] = args[0] |
|
except IndexError: |
|
if app.config['DEEPZOOM_SLIDE'] is None: |
|
parser.error('No slide file specified') |
|
|
|
app.run(host=opts.host, port=opts.port, threaded=True) |