PK ! ы imserv/__init__.pyfrom flask import Flask app = Flask(__name__) from .views import index, get_image from .api import create_image from .main import ImServ PK ! t imserv/__main__.pyimport click from .main import ImServ @click.command() @click.option('--port', default=29635) def runserver(port): ImServ(port=port).runserver() if __name__ == '__main__': runserver() PK ! 0[ imserv/api.pyfrom flask import request, jsonify, make_response, abort from werkzeug.utils import secure_filename from pathlib import Path from io import BytesIO from . import app, db @app.route('/api/images/create', methods=['POST']) def create_image(): if 'file' in request.files: tags = request.form.get('tags') file = request.files['file'] with BytesIO() as bytes_io: file.save(bytes_io) db_image = db.Image.from_bytes_io(bytes_io, filename=secure_filename(file.filename), tags=tags) if isinstance(db_image, str): return abort(make_response(jsonify(message=db_image), 409)) else: return jsonify({ 'filename': str(Path(db_image.filename).name), 'trueFilename': str(db_image.filename) }), 201 response = make_response() response.status_code = 304 return response PK ! x imserv/config.pyimport os from pathlib import Path config = { 'database': 'imserv', 'host': 'localhost', 'port': 29635, 'debug': False, 'threaded': False, 'hash_size': 32, 'hash_difference_threshold': 0 } for k in config.keys(): env_k = 'IMSERV_' + k.upper() if env_k in os.environ.keys(): v = os.environ[env_k] if k in {'port', 'hash_size', 'hash_difference_threshold', 'skip_hash'}: config[k] = int(v) elif k in {'debug', 'threaded'}: config[k] = bool(v) else: config[k] = v if config.get('folder', None) is None: OS_IMG_FOLDER_PATH = Path.home().joinpath('Pictures') assert OS_IMG_FOLDER_PATH.exists() IMG_FOLDER_PATH = OS_IMG_FOLDER_PATH.joinpath('imserv') IMG_FOLDER_PATH.mkdir(exist_ok=True) config['folder'] = IMG_FOLDER_PATH else: config['folder'] = Path(config['folder']) config['blob_folder'] = config['folder'].joinpath('blob') config['blob_folder'].mkdir(exist_ok=True) PK ! ڇ imserv/db.pyimport peewee as pv import imagehash from uuid import uuid4 from nonrepeat import nonrepeat_filename from slugify import slugify import PIL.Image import os import sys from datetime import datetime from urllib.parse import quote, urlparse from .config import config from .util import get_checksum, get_image_hash __all__ = ('Tag', 'Image', 'ImageTags', 'create_all_tables') class BaseModel(pv.Model): class Meta: database = pv.PostgresqlDatabase(config['database']) class ImageHashField(pv.TextField): def db_value(self, value): if value: return str(value) def python_value(self, value): if value: return imagehash.hex_to_hash(value) class Tag(BaseModel): name = pv.TextField() def to_json(self): return { 'name': self.name, 'images': [img.to_json() for img in self.images] } class Image(BaseModel): file_id = pv.IntegerField(null=True) filename = pv.TextField(null=False) checksum = pv.TextField(null=False) image_hash = ImageHashField(null=True) tags = pv.ManyToManyField(Tag, backref='images') @classmethod def from_bytes_io(cls, im_bytes_io, filename=None, tags=None): """ :param im_bytes_io: :param str filename: :param str|list|tuple tags: :return: """ if tags is None: tags = list() elif isinstance(tags, str): tags = [tags] if not filename or filename == 'image.png': filename = slugify('-'.join(tags)) + str(uuid4())[:8] + '.png' filename = nonrepeat_filename(filename, root=str(config['blob_folder'])) filename = str(config['blob_folder'].joinpath(filename)) checksum = get_checksum(im_bytes_io) im = PIL.Image.open(im_bytes_io) image_hash = get_image_hash(im) im.save(filename) db_image = cls.create( file_id=os.stat(filename).st_ino, filename=filename, checksum=checksum, image_hash=image_hash ) for tag in set(tags): db_image.tags.add(Tag.get_or_create(name=tag)[0]) return db_image @classmethod def from_existing(cls, filename, tags=None): if tags is None: tags = list() filename = str(filename) db_image = cls.create( file_id=os.stat(filename).st_ino, filename=filename, checksum=get_checksum(filename), image_hash=get_image_hash(filename) ) for tag in tags: db_image.tags.add(Tag.get_or_create(name=tag)[0]) db_image.path = filename return db_image @classmethod def similar_images(cls, im): image_hash = get_image_hash(im) if config['hash_difference_threshold']: for db_image in cls.select(): if db_image.image_hash - image_hash < config['hash_difference_threshold']: yield db_image else: return cls.select().where(cls.image_hash == image_hash) def get_image(self, max_width=800, max_height=800): url = self.url if url: return f'' def _repr_html_(self): return self.get_image(800, 800) def _repr_json_(self): result = self.to_json() result['image'] = self.filename return result @property def url(self): if not urlparse(self.filename).netloc: return 'http://{}:{}/images?filename={}'.format( config['host'], config['port'], quote(str(self.filename), safe='') ) else: return self.filename def to_json(self): return dict( id=self.id, image=self.get_image(400, 400), filename=self.filename, notes=[n.data for n in self.notes], tags=[t.name for t in self.tags] ) handsontable_config = { 'renderers': { 'image': 'html' }, 'config': { 'colWidths': { 'image': 400 } } } ImageTags = Image.tags.get_through_model() class Note(BaseModel): image = pv.ForeignKeyField(Image, backref='notes') data = pv.TextField() def _repr_markdown_(self): return self.data def to_json(self): return { 'file_id': self.file_.to_json(), 'data': self.data } def create_all_tables(): for cls in sys.modules[__name__].__dict__.values(): if hasattr(cls, '__bases__') and issubclass(cls, pv.Model): if cls is not BaseModel: cls.create_table() PK ! m6 6 imserv/main.pyimport sys import re from threading import Thread import subprocess from tqdm import tqdm from watchdog.observers import Observer from watchdog.events import FileSystemEventHandler from pathlib import Path import random from .config import config from .util import open_browser_tab, images_in_path, get_checksum, get_image_hash from . import app __all__ = ('ImServ',) class FileCreationHandler(FileSystemEventHandler): def __init__(self, expected_total): self.tqdm = tqdm( total=expected_total ) def on_created(self, event): self.tqdm.update() class ImServ: def __init__(self, **kwargs): config.update(kwargs) from . import db self.db = dict( image=db.Image, note=db.Note, tag=db.Tag ) def __getitem__(self, item): return self.db[item] def runserver(self): """Run the image server (see README.md) """ def _runserver(): app.run( host=config['host'], port=config['port'], debug=config['debug'] ) def _runserver_in_thread(): open_browser_tab('http://{}:{}'.format( config['host'], config['port'] )) self.server_thread = Thread(target=_runserver) self.server_thread.daemon = True self.server_thread.start() if config['threaded'] or 'ipykernel' in ' '.join(sys.argv): _runserver_in_thread() else: _runserver() def search_filename(self, filename_regex, calculate_hash=False): def _search(): for file_path in images_in_path(): if re.search(filename_regex, str(file_path), flags=re.IGNORECASE): db_image = self._get_or_create(file_path, calculate_hash) yield db_image return sorted(_search(), key=lambda x: x.filename) def search_database(self, query): return self.db['image'].select().where(query) def refresh(self, calculate_hash=False): for file_path in tqdm(tuple(images_in_path())): db_image = self._get_or_create(file_path, calculate_hash) if db_image is not None: if calculate_hash: db_image.image_hash = get_image_hash(file_path) db_image.save() def _get_or_create(self, file_path, calculate_hash): checksum = get_checksum(file_path) image_hash = None if calculate_hash: image_hash = get_image_hash(file_path) db_image = self.db['image'].get_or_none( file_id=file_path.stat().st_ino ) if db_image is None: db_image = self.db['image'].create( file_id=file_path.stat().st_ino, checksum=checksum, image_hash=image_hash, filename=str(file_path) ) else: if image_hash: db_image.image_hash = image_hash if checksum != db_image.checksum: db_image.checksum = checksum db_image.filename = str(file_path) db_image.save() return db_image def import_pdf(self, pdf_filename, calculate_hash=False): """ Import images from a PDF. Poppler (https://poppler.freedesktop.org) will be required. In Mac OSX, `brew install poppler`. In Linux, `yum install poppler-utils` or `apt-get install poppler-utils`. Arguments: pdf_filename {str, pathlib.Path} -- Path to PDF file. """ def _extract_pdf(): filename_initials = ''.join(c for c in str(Path(pdf_filename).name) if c.isupper()) if not filename_initials: filename_initials = pdf_filename[0] number_of_images = len(subprocess.check_output([ 'pdfimages', '-list', str(pdf_filename) ]).split(b'\n')) - 2 observer = Observer() event_handler = FileCreationHandler(expected_total=number_of_images) observer.schedule(event_handler, str(dst_folder_path), recursive=False) observer.setDaemon(True) observer.start() try: subprocess.call([ 'pdfimages', '-p', '-png', str(pdf_filename), str(dst_folder_path.joinpath(filename_initials)) ]) observer.stop() except KeyboardInterrupt: observer.stop() observer.join() event_handler.tqdm.close() dst_folder_path = config['folder'].joinpath('pdf').joinpath(Path(pdf_filename).stem) if not dst_folder_path.exists(): dst_folder_path.mkdir(parents=True) _extract_pdf() for file_path in tqdm(tuple(images_in_path(dst_folder_path))): self._get_or_create(file_path, calculate_hash=calculate_hash) def get_pdf_image(self, filename_regex, page_start, page_end, calculate_hash=True, randomize=False): """Search images corresponding to PDF in config['folder'] Arguments: filename_regex {str} -- Regex matching the PDF filename page_start {int} -- First page to search page_end {int} -- Last page to search Yields: db.Image object corresponding to the criteria """ def _get_image(): for file_path in images_in_path(): match_obj = re.search(rf'{filename_regex}.*(?:[^\d])(\d+)-\d+\.png', str(file_path), flags=re.IGNORECASE) if match_obj is not None: page_number = int(match_obj.group(1)) if page_number in range(page_start, page_end + 1): db_image = self._get_or_create(file_path, calculate_hash) yield page_number, db_image if randomize: images = [db_image for i, db_image in _get_image()] random.shuffle(images) return images else: return [db_image for i, db_image in sorted(_get_image(), key=lambda x: x[0])] def last_created(self): return self.db['image'].select().order_by(self.db['image'].id.desc()) PK ! " imserv/static/index.cssinput { font-size: 24px; margin: 0 auto; margin-top: 20px; } #tags-area, #input-bar { width: 500px; } #tags-bar { width: 100%; } img { margin-top: 20px; } PK ! \ imserv/static/index.jsconst tagsBar = document.getElementById('tags-bar'); const inputBar = document.getElementById('input-bar'); const imageCanvas = document.getElementById('image-canvas'); let tags = JSON.parse(localStorage.getItem('tags') || '[]'); let imagePath = ''; tagsBar.value = joinTags(); inputBar.value = imagePath; inputBar.onpaste = ()=>{ const items = (event.clipboardData || event.originalEvent.clipboardData).items; // console.log(items); // will give you the mime types for (index in items) { const item = items[index]; if (item.kind === 'file') { const file = item.getAsFile(); let reader = new FileReader(); reader.onload = function(event) { const extension = file.type.match(/\/([a-z0-9]+)/i)[1].toLowerCase(); let formData = new FormData(); formData.append('file', file, file.name); formData.append('extension', extension); formData.append('mimetype', file.type); formData.append('submission-type', 'paste'); // formData.append('imagePath', imagePath); formData.append('tags', tags); localStorage.setItem('tags', JSON.stringify(tags)); fetch('/api/images/create', { method: 'POST', body: formData }).then(response=>response.json()) .then(responseJson=>{ if(responseJson.filename){ inputBar.value = responseJson.filename; imageCanvas.src = '/images?filename=' + encodeURIComponent(responseJson.trueFilename); } else { alert(responseJson.message); } }) .catch(error => { console.error(error); }); }; reader.readAsBinaryString(file); } } } tagsBar.addEventListener("keyup", function(event) { function purge(tag){ tag = tag.trim(); if(tag){ tags.push(tag); } } tags = []; let purgable = true; let currentTag = '' tagsBar.value.split('').forEach((character, index)=>{ if(character === ',' && purgable){ if(purgable){ purge(currentTag); currentTag = ''; } else { currentTag += character; } } else if (character === '\"'){ purgable = !purgable; } else { currentTag += character; } }); purge(currentTag); }); function joinTags(){ let result = [] tags.forEach((tag, index)=>{ if(tag.indexOf(',') !== -1){ result.push('\"' + tag + '\"'); } else { result.push(tag); } }); return result.join(', ') }PK ! imserv/templates/index.html