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()
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 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