├── .gitignore ├── Dockerfile ├── LICENSE ├── Procfile ├── README.md ├── app.py ├── docker-compose.yml ├── environment.yml ├── example ├── fakecatalog.csv └── images │ ├── ngc450.jpg │ ├── ngc5792.jpg │ └── ngc60.jpg ├── pager.py ├── requirements.txt ├── screenshot.png ├── supervisord.conf ├── templates ├── 404.html ├── imageview.html ├── layout.html ├── links.html └── table.html └── uwsgi.ini /.gitignore: -------------------------------------------------------------------------------- 1 | *.pyc 2 | *~ 3 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM python:3.9.10-slim-buster as compile-image 2 | 3 | RUN apt-get update && \ 4 | apt-get install -y --no-install-recommends \ 5 | build-essential python3-dev apt-utils && \ 6 | rm -rf /var/lib/apt/lists/* 7 | 8 | WORKDIR /opt/venv 9 | 10 | # Create virtual environment 11 | RUN python3 -m venv . 12 | 13 | # Make sure virtualenv is used 14 | ENV PATH="/opt/venv/bin:$PATH" 15 | 16 | COPY ./requirements.txt ./ 17 | RUN pip install --upgrade pip && \ 18 | pip3 install -r requirements.txt 19 | 20 | # --------------------------------------- 21 | 22 | FROM python:3.9.10-slim-buster as running-image 23 | 24 | RUN apt-get update && \ 25 | apt-get install -y --no-install-recommends \ 26 | supervisor && \ 27 | rm -rf /var/lib/apt/lists/* 28 | WORKDIR /opt/app 29 | COPY . . 30 | COPY --from=compile-image /opt/venv ./venv 31 | RUN useradd appuser 32 | RUN chown -R appuser /opt/app 33 | COPY supervisord.conf /etc/ 34 | RUN chmod 0644 /etc/supervisord.conf 35 | 36 | USER appuser 37 | ENV PATH="/opt/app/venv/bin:$PATH" 38 | 39 | CMD ["/usr/bin/supervisord"] 40 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 Semyenog Oh 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Procfile: -------------------------------------------------------------------------------- 1 | web: gunicorn app:app --log-file=- 2 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ImageViewer 2 | 3 | Do you have a bunch of images with meta information that you would like to go through (or share)? 4 | 5 | This is a web-based image viewer for that. 6 | 7 | It is a tiny [flask](http://flask.pocoo.org/) app, built with [bootstrap](http://getbootstrap.com/), 8 | which will display images and their meta information from a table with pagination. It could be a starting point for a more complex application. 9 | 10 | **WARNING:** This app was built to run locally for my own convenience and 11 | to be shared with a small number of collaborators. In `app.py`, the entire table is 12 | loaded into a global variable `table`. This can be problematic if the table is large, 13 | and the app receives a lot of traffic, as it will load the data for every process. 14 | You'll want to setup a proper database. See 15 | 16 | - [this stackoverflow question](http://stackoverflow.com/questions/28141454/flask-using-a-global-variable-to-load-data-files-into-memory) 17 | - flask documentation on databases 18 | 19 | 20 | ## Test out 21 | 22 | You can either run in a new conda environment or use docker. 23 | 24 | 1. Create a new conda environment to install required packages locally: 25 | 26 | ```sh 27 | git clone https://github.com/smoh/imageviewer 28 | cd imageviewer 29 | conda env create # will create 'viewer' environment specified in environment.yml 30 | source activate viewer 31 | python app.py 32 | ``` 33 | 34 | 2. Use [docker](https://www.docker.com/): 35 | ```sh 36 | git clone https://github.com/smoh/imageviewer 37 | cd imageviewer 38 | docker-compose up --build 39 | ``` 40 | 41 | Once the app is running, point your browser to `localhost:5000`. 42 | This will show images inside `example/images/` directory with the related information in `fakecatalog.csv` 43 | like this: 44 | 45 | ![](screenshot.png) 46 | 47 | Configure static file paths in `app.py` and modify templates in `templates/` 48 | according to your needs. [Flask](http://flask.pocoo.org/) uses the [jinja](http://jinja.pocoo.org/) template engine. 49 | -------------------------------------------------------------------------------- /app.py: -------------------------------------------------------------------------------- 1 | import csv 2 | from flask import Flask, render_template, request, redirect, url_for 3 | import requests 4 | from pager import Pager 5 | 6 | 7 | def read_table(url): 8 | """Return a list of dict""" 9 | # r = requests.get(url) 10 | with open(url) as f: 11 | return [row for row in csv.DictReader(f.readlines())] 12 | 13 | 14 | APPNAME = "PrettyGalaxies" 15 | STATIC_FOLDER = 'example' 16 | TABLE_FILE = "example/fakecatalog.csv" 17 | 18 | table = read_table(TABLE_FILE) 19 | pager = Pager(len(table)) 20 | 21 | 22 | app = Flask(__name__, static_folder=STATIC_FOLDER) 23 | app.config.update( 24 | APPNAME=APPNAME, 25 | ) 26 | 27 | 28 | @app.route('/') 29 | def index(): 30 | return redirect('/0') 31 | 32 | 33 | @app.route('//') 34 | def image_view(ind=None): 35 | if ind >= pager.count: 36 | return render_template("404.html"), 404 37 | else: 38 | pager.current = ind 39 | return render_template( 40 | 'imageview.html', 41 | index=ind, 42 | pager=pager, 43 | data=table[ind]) 44 | 45 | 46 | @app.route('/goto', methods=['POST', 'GET']) 47 | def goto(): 48 | return redirect('/' + request.form['index']) 49 | 50 | 51 | if __name__ == '__main__': 52 | app.run(debug=True) 53 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3.8' 2 | services: 3 | imageviewer: 4 | build: . 5 | restart: on-failure 6 | ports: 7 | - 5000:5000/tcp 8 | -------------------------------------------------------------------------------- /environment.yml: -------------------------------------------------------------------------------- 1 | name: viewer 2 | 3 | dependencies: 4 | - python=3 5 | - pip: 6 | - flask 7 | - requests 8 | 9 | -------------------------------------------------------------------------------- /example/fakecatalog.csv: -------------------------------------------------------------------------------- 1 | name,ra,dec,notes 2 | ngc450,18.876,-0.861,"not a physical companion" 3 | ngc5792,224.594,-1.09,"a spiral galaxy" 4 | ngc60,3.992,-0.303,"distorted spiral arms" 5 | 6 | -------------------------------------------------------------------------------- /example/images/ngc450.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/smoh/imageviewer/06cb0db5ee004a8e99c5018156ae8a659612d4d6/example/images/ngc450.jpg -------------------------------------------------------------------------------- /example/images/ngc5792.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/smoh/imageviewer/06cb0db5ee004a8e99c5018156ae8a659612d4d6/example/images/ngc5792.jpg -------------------------------------------------------------------------------- /example/images/ngc60.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/smoh/imageviewer/06cb0db5ee004a8e99c5018156ae8a659612d4d6/example/images/ngc60.jpg -------------------------------------------------------------------------------- /pager.py: -------------------------------------------------------------------------------- 1 | 2 | class Pager(object): 3 | def __init__(self, count): 4 | self.count = count 5 | self.current = 0 6 | 7 | @property 8 | def next(self): 9 | n = self.current + 1 10 | if n > self.count-1: 11 | n -= self.count 12 | return n 13 | 14 | @property 15 | def prev(self): 16 | n = self.current - 1 17 | if n < 0 : 18 | n += self.count 19 | return n -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | # packages in environment at /Users/semyeong/anaconda/envs/webapp: 2 | # 3 | 4 | flask 5 | gunicorn 6 | jinja2 7 | werkzeug 8 | requests 9 | uwsgi 10 | -------------------------------------------------------------------------------- /screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/smoh/imageviewer/06cb0db5ee004a8e99c5018156ae8a659612d4d6/screenshot.png -------------------------------------------------------------------------------- /supervisord.conf: -------------------------------------------------------------------------------- 1 | 2 | [supervisord] 3 | nodaemon=true 4 | user=appuser 5 | 6 | [program:uwsgi] 7 | command=/opt/app/venv/bin/uwsgi --ini /opt/app/uwsgi.ini --die-on-term 8 | stdout_logfile=/dev/stdout 9 | stdout_logfile_maxbytes=0 10 | stderr_logfile=/dev/stderr 11 | stderr_logfile_maxbytes=0 -------------------------------------------------------------------------------- /templates/404.html: -------------------------------------------------------------------------------- 1 | {% extends "layout.html" %} 2 | 3 | {% block content %} 4 |
5 |
6 |
7 |

Oops!

8 |

404 Not Found

9 |
10 | Sorry, Requested page not found! 11 |
12 |
13 |
14 |
15 | {% endblock %} 16 | -------------------------------------------------------------------------------- /templates/imageview.html: -------------------------------------------------------------------------------- 1 | {% extends "layout.html" %} 2 | 3 | {% block title %}{{ data.name}} {% endblock %} 4 | 5 | {% block content %} 6 |
7 | 8 | 26 | 27 |
28 |
29 | 30 |
31 |
32 |

{{data.name}}

33 | {% include 'table.html' %} 34 |
35 |
36 | 37 |
38 | 39 | {% endblock %} 40 | -------------------------------------------------------------------------------- /templates/layout.html: -------------------------------------------------------------------------------- 1 | {# base template #} 2 | 3 | 4 | 5 | 6 | {% block head %} 7 | {% block title %}{% endblock %} - {{ config.APPNAME }} 8 | {% endblock %} 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 |
{% block content %}{% endblock %}
22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /templates/links.html: -------------------------------------------------------------------------------- 1 | {# add other useful links here 2 | These links appears in the navbar #} 3 | 4 |
  • SDSS
  • 5 | -------------------------------------------------------------------------------- /templates/table.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | {% for key in ["name", "ra", "dec", "notes"] %} 4 | 5 | 6 | 7 | 8 | {% endfor %} 9 | 10 |
    {{ key|e|capitalize }}{{ data[key]|e }}
    11 | 12 | -------------------------------------------------------------------------------- /uwsgi.ini: -------------------------------------------------------------------------------- 1 | [uwsgi] 2 | module = app:app 3 | chdir=/opt/app 4 | uid = appuser 5 | gid = appuser 6 | buffer-size = 8196 7 | master = True 8 | vhost = True 9 | #http-socket = 0.0.0.0:5000 # Use http-socket if using Nginx (uwsgi_pass) 10 | http = 0.0.0.0:5000 # Use http if exposing uWSGI directly to the internet 11 | chown-socket = appuser:appuser 12 | chmod-socket = 664 13 | vacuum = true 14 | threads = 2 15 | processes = %(%k + 1) 16 | workers = 2 --------------------------------------------------------------------------------