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! timserv/__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!ximserv/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() if not filename or filename == 'image.png': filename = str(uuid4())[:8] + '.png' filename = nonrepeat_filename(filename, primary_suffix=slugify('-'.join(tags)), 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 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!^c))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 tqdm(tuple(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()) 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

PK!tB  imserv/util.pyfrom threading import Thread from time import sleep import webbrowser from PIL import Image, ImageChops from pathlib import Path import imagehash from send2trash import send2trash import hashlib from io import BytesIO import logging from .config import config def open_browser_tab(url): def _open_tab(): sleep(1) webbrowser.open_new_tab(url) thread = Thread(target=_open_tab) thread.daemon = True thread.start() def trim_image(im): bg = Image.new(im.mode, im.size, im.getpixel((0,0))) diff = ImageChops.difference(im, bg) diff = ImageChops.add(diff, diff, 2.0, -100) bbox = diff.getbbox() if bbox: im = im.crop(bbox) return im def shrink_image(im, max_width=800): width, height = im.size if width > max_width: im.thumbnail((max_width, height * max_width / width)) return im def remove_duplicate(file_path=config['folder']): hashes = set() for p in images_in_path(file_path): h = get_image_hash(Image.open(p), trim=True) if h in hashes: print('Deleting {}'.format(p)) send2trash(p) else: hashes.add(h) def remove_non_images(file_path=config['folder']): images = set(images_in_path(file_path)) for p in Path(file_path).glob('**/*.*'): if not p.is_dir() and p not in images: send2trash(str(p)) def images_in_path(file_path=config['folder']): for p in Path(file_path).glob('**/*.*'): if not p.is_dir() and not p.name.startswith('.') \ and p.suffix.lower() in {'.png', '.jpg', '.jp2', '.jpeg', '.gif'}: yield p def complete_path_split(path, relative_to=config['folder']): components = [] path = Path(path) if relative_to: path = path.relative_to(relative_to) while path.name: components.append(path.name) path = path.parent return components def get_image_hash(im, trim=True, **kwargs): if isinstance(im, (str, Path)): try: im = Image.open(im) except OSError: return None if trim: im = trim_image(im) return imagehash.whash( im, hash_size=config['hash_size'], **kwargs ) def get_checksum(fp): if isinstance(fp, BytesIO): checksum = hashlib.md5(fp.getvalue()).hexdigest() elif isinstance(fp, (str, Path)): checksum = hashlib.md5(Path(fp).read_bytes()).hexdigest() else: logging.error('Cannot generate checksum') return None return checksum PK!R~/imserv/views.pyfrom flask import render_template, send_file, request from urllib.parse import unquote from pathlib import Path from .config import config from . import app @app.route('/') def index(): return render_template('index.html') @app.route('/images') def get_image(): file_path = Path(unquote(request.args.get('filename'))) if not file_path.exists(): file_path = config['folder'].joinpath(file_path) print('Serving: {}'.format(file_path)) return send_file(str(file_path)) PK!H=/4(imserv-0.1.11.dist-info/entry_points.txtN+I/N.,()-N-*Pz񹉙yVEy "..PK! ::imserv-0.1.11.dist-info/LICENSEMIT License Copyright (c) 2018 Pacharapol Withayasakpunt Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. PK!H WXimserv-0.1.11.dist-info/WHEEL A н#Z."jm)Afb~ڠO68oF04UhoAf f4=4h0k::wXPK!H(0M# imserv-0.1.11.dist-info/METADATAVKs6Wl'H3)ԍQh+1 0hV=wP؃xD.|N‰h*i8 D¢y n$L(YEpSJBN,fi>䲌0)J\$8 JUD8gaI>"Ȝ+m4-˪8Lt1*)c#Jף6 < +iL$d,VŠRE 2/K>\X+Q>ߜqY)S+K#ʅ sDي"+5oin[|{[[DZABp<8xhFL;MffanJieD `v+kDQƒ.s]3d7 vr9Rѐ1vua6rNҩ3T^7pIggf3mbd#;1ޮJCT*yqsK#xCqu"ϛ \FFyҝU1_G 5J)&֗yёu$JKȣZ׭쩊`>{&4_6Cމ8Gp볗n3U[PSA3>,-6 <}U -ZK u).DH.,̴uo~h($.=#)tHe +]A&N\X ゛TJqz \ZJh`8sv=:<k}+nilD+j:Dz͊ydIPK!Himserv-0.1.11.dist-info/RECORDuےkj,c.օ s#<]ʮ_9FMPi?}!h ] C)w~9^)9#`RXzz^$W":N~M#Gz;Ye8(su|.%sg9zհp છ@<Þw {ljQgK$ɒcb'y5aLUHpd u'2?#4`AG%ðyO\xuʪwXkzMi {ٗyJ,3rŷvk*zU`Kku~p,_UhECԞ8]t#氮6SpTpe φqoΡ[kٞ.GoڪK"To rx:.'#RkEW/1Gmd41Hq2G۫*xsF0lުs_|IPV?`(hG~ 4pSvb;gک2J