├── setup.cfg ├── photorepl ├── views │ ├── __init__.py │ └── preview.py ├── __init__.py ├── threads.py ├── __main__.py └── photo.py ├── MANIFEST.in ├── Pipfile ├── .pre-commit-config.yaml ├── Makefile ├── README.rst ├── .gitignore ├── LICENSE └── setup.py /setup.cfg: -------------------------------------------------------------------------------- 1 | [bdist_wheel] 2 | universal = 1 3 | -------------------------------------------------------------------------------- /photorepl/views/__init__.py: -------------------------------------------------------------------------------- 1 | # Make PyPy happy 2 | -------------------------------------------------------------------------------- /photorepl/__init__.py: -------------------------------------------------------------------------------- 1 | version = '0.0.3' 2 | app_name = 'photoREPL' 3 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include LICENSE 2 | include *.rst 3 | include *.txt 4 | include Makefile 5 | recursive-include *.py 6 | -------------------------------------------------------------------------------- /Pipfile: -------------------------------------------------------------------------------- 1 | [[source]] 2 | url = "https://pypi.org/simple" 3 | verify_ssl = true 4 | name = "pypi" 5 | 6 | [packages] 7 | rawkit = "*" 8 | pgi = "*" 9 | 10 | [dev-packages] 11 | 12 | [requires] 13 | python_version = "3" 14 | -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | - repo: git://github.com/pre-commit/pre-commit-hooks 2 | sha: v0.4.2 3 | hooks: 4 | - id: autopep8-wrapper 5 | args: ['-i'] 6 | - id: check-json 7 | - id: check-yaml 8 | - id: name-tests-test 9 | - id: debug-statements 10 | - id: end-of-file-fixer 11 | - id: trailing-whitespace 12 | - id: check-case-conflict 13 | - id: check-merge-conflict 14 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | RUN=pipenv run 2 | 3 | .PHONY: run 4 | run: 5 | $(RUN) python -i -m photorepl 6 | 7 | dist/*.whl: setup.py photorepl/*.py Pipfile Pipfile.lock 8 | $(RUN) python setup.py bdist_wheel 9 | 10 | dist/*.tar.gz: setup.py photorepl/*.py Pipfile Pipfile.lock 11 | $(RUN) python setup.py sdist bdist 12 | 13 | .PHONY: wheel 14 | wheel: dist/*.whl 15 | 16 | .PHONY: dist 17 | dist: dist/*.tar.gz 18 | 19 | .PHONY: upload 20 | upload: clean 21 | $(RUN) python setup.py sdist bdist bdist_wheel upload 22 | 23 | .PHONY: clean 24 | clean: 25 | find . -iname '*.pyc' | xargs rm -f 26 | find . -iname '__pycache__' -type d | xargs rm -rf 27 | rm -rf build 28 | rm -rf dist 29 | rm -rf *.egg-info 30 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | photoREPL 2 | ========= 3 | 4 | ``photoREPL`` is an experimental UI build around the rawkit_ (docs_) raw photo 5 | editing library for Python. 6 | 7 | photoREPL drops you at a Python prompt, with a few custom functions and a copy 8 | of rawkit imported, and spawns a preview window for any photos you edit. As you 9 | make changes to your photos, the image in the preview window is updated to 10 | reflect those changes, giving you near real time editing directly from Python 11 | without a lot of extra update calls! 12 | 13 | To run try: :: 14 | 15 | make run 16 | 17 | or run it directly: :: 18 | 19 | python -i -m photorepl [some_raw_photos] 20 | 21 | you can also install it via pip: :: 22 | 23 | pip install photorepl 24 | 25 | .. _rawkit: https://github.com/photoshell/rawkit 26 | .. _docs: https://rawkit.readthedocs.org/ 27 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Thank you GitHub: https://github.com/github/gitignore/blob/master/Python.gitignore 2 | 3 | # Byte-compiled / optimized / DLL files 4 | __pycache__/ 5 | *.py[cod] 6 | 7 | # C extensions 8 | *.so 9 | 10 | # Distribution / packaging 11 | .Python 12 | env/ 13 | build/ 14 | develop-eggs/ 15 | dist/ 16 | downloads/ 17 | eggs/ 18 | lib/ 19 | lib64/ 20 | parts/ 21 | sdist/ 22 | var/ 23 | *.egg-info/ 24 | .installed.cfg 25 | *.egg 26 | 27 | # PyInstaller 28 | # Usually these files are written by a python script from a template 29 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 30 | *.manifest 31 | *.spec 32 | 33 | # Installer logs 34 | pip-log.txt 35 | pip-delete-this-directory.txt 36 | 37 | # Unit test / coverage reports 38 | htmlcov/ 39 | .tox/ 40 | .coverage 41 | .cache 42 | nosetests.xml 43 | coverage.xml 44 | 45 | # Translations 46 | *.mo 47 | *.pot 48 | 49 | # Django stuff: 50 | *.log 51 | 52 | # Sphinx documentation 53 | docs/_build/ 54 | 55 | # PyBuilder 56 | target/ 57 | 58 | # Vim 59 | *.sw[po] 60 | 61 | test-results/ 62 | Pipfile.lock 63 | -------------------------------------------------------------------------------- /photorepl/threads.py: -------------------------------------------------------------------------------- 1 | import threading 2 | 3 | from gi.repository import Gtk, Gdk 4 | from photorepl.views.preview import Preview 5 | 6 | 7 | class UIThread(threading.Thread): 8 | 9 | """ 10 | A thread for displaying UI elements and photos. This thread shouldn't 11 | maintain any state which must be preserved, and should act as a daemon 12 | thread which exits when the main thread (the REPL) is terminated. 13 | """ 14 | 15 | def __init__(self): 16 | """ 17 | Initialize the ui thead, making sure it's a daemon thread which will 18 | exit when the main thread is terminated. 19 | """ 20 | 21 | super(UIThread, self).__init__() 22 | self.daemon = True 23 | 24 | def run(self): 25 | """ 26 | Create the preview window and run the Gtk main loop when the UI thead 27 | is started. 28 | """ 29 | Gdk.threads_init() 30 | 31 | try: 32 | Gdk.threads_enter() 33 | Gtk.main() 34 | finally: 35 | Gdk.threads_leave() 36 | 37 | def open_window(self, filename=None, rawfile=None): 38 | """ 39 | Open a new preview window with the given preview file and raw file. 40 | """ 41 | return Preview(filename=filename, rawfile=rawfile, show=True) 42 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright © 2014 Sam Whited 2 | 3 | Redistribution and use in source and binary forms, with or without 4 | modification, are permitted provided that the following conditions are met: 5 | 6 | 1. Redistributions of source code must retain the above copyright notice, this 7 | list of conditions and the following disclaimer. 8 | 9 | 2. Redistributions in binary form must reproduce the above copyright notice, 10 | this list of conditions and the following disclaimer in the documentation 11 | and/or other materials provided with the distribution. 12 | 13 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 14 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 15 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 16 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 17 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 18 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 19 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 20 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 21 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 22 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 23 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | from setuptools import setup 4 | from photorepl import version 5 | 6 | 7 | def readme(): 8 | with open(os.path.join(os.path.dirname(__file__), 'README.rst')) as f: 9 | return f.read() 10 | 11 | setup( 12 | name='photoREPL', 13 | version=version, 14 | description='Experimental CLI/GUI hybrid raw photo editor', 15 | author='Sam Whited', 16 | author_email='sam@samwhited.com', 17 | maintainer='Sam Whited', 18 | maintainer_email='sam@samwhited.com', 19 | url='https://github.com/photoshell/photorepl', 20 | packages=['photorepl'], 21 | keywords=[ 22 | 'encoding', 'images', 'photography', 'libraw', 'raw', 'photos', 23 | 'editing', 'graphics' 24 | ], 25 | classifiers=[ 26 | "Programming Language :: Python", 27 | "Programming Language :: Python :: 3", 28 | "Programming Language :: Python :: 3.3", 29 | "Programming Language :: Python :: 3.4", 30 | "Programming Language :: Python :: 3 :: Only", 31 | "Programming Language :: Python :: Implementation :: CPython", 32 | "Programming Language :: Python :: Implementation :: PyPy", 33 | "Development Status :: 3 - Alpha", 34 | "Intended Audience :: End Users/Desktop", 35 | "License :: OSI Approved :: BSD License", 36 | "Operating System :: POSIX", 37 | "Operating System :: POSIX :: Linux", 38 | "Operating System :: Unix", 39 | "Topic :: Multimedia :: Graphics :: Editors :: Raster-Based", 40 | "Topic :: Multimedia :: Graphics :: Viewers", 41 | "Environment :: X11 Applications :: GTK", 42 | ], 43 | long_description=readme(), 44 | require=[ 45 | 'rawkit' 46 | ], 47 | extras_require={ 48 | 'GI (when in a venv or gi.repository is not available)': ['pgi >= 0.0.10.1'] 49 | }, 50 | ) 51 | -------------------------------------------------------------------------------- /photorepl/views/preview.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | from gi.repository import Gtk, GdkPixbuf 4 | from photorepl import app_name 5 | from threading import Lock 6 | 7 | 8 | class Preview(Gtk.Window): 9 | 10 | """ 11 | The preview window, which shows photos as you edit them. 12 | """ 13 | 14 | def __init__(self, filename=None, rawfile=None, show=True): 15 | super().__init__() 16 | 17 | self.set_wmclass(app_name, app_name) 18 | self.rawfile = rawfile 19 | self.set_title(app_name) 20 | self.set_default_size(800, 600) 21 | 22 | self.settings = Gtk.Settings.get_default() 23 | 24 | try: 25 | self.settings.set_property( 26 | 'gtk-application-prefer-dark-theme', 27 | True 28 | ) 29 | except TypeError: 30 | # Can't do this if we're using pgi for some reason. 31 | pass 32 | 33 | self.box = Gtk.Box.new(Gtk.Orientation.HORIZONTAL, 0) 34 | 35 | self.mutex = Lock() 36 | 37 | if filename is not None: 38 | self.render_photo(filename) 39 | 40 | self.add(self.box) 41 | 42 | if show: 43 | self.show_all() 44 | 45 | def set_title(self, title): 46 | """ 47 | Sets the title of the preview window. 48 | """ 49 | 50 | if self.rawfile is None: 51 | super().set_title(title) 52 | else: 53 | super().set_title('{} — {}'.format( 54 | title, 55 | os.path.basename(self.rawfile) 56 | )) 57 | 58 | def render_photo(self, filename): 59 | """ 60 | Renders the given photo (generally in PGM format) in the preview 61 | window. 62 | """ 63 | 64 | self.mutex.acquire() 65 | 66 | self.set_title(app_name) 67 | 68 | try: 69 | self.box.remove(self.image) 70 | except AttributeError: 71 | pass 72 | 73 | pix = GdkPixbuf.Pixbuf.new_from_file_at_size(filename, 800, 600) 74 | self.image = Gtk.Image.new_from_pixbuf(pix) 75 | self.box.pack_start(self.image, True, True, 0) 76 | self.show_all() 77 | 78 | self.mutex.release() 79 | -------------------------------------------------------------------------------- /photorepl/__main__.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | import atexit 4 | import photorepl 5 | import sys 6 | import threading 7 | 8 | try: 9 | import pgi 10 | pgi.install_as_gi() 11 | except ImportError: 12 | pass 13 | 14 | from gi.repository import GLib 15 | from photorepl.threads import UIThread 16 | from .photo import Photo 17 | 18 | 19 | def edit(filename, cache=True): 20 | """ 21 | Opens the given filename and spawns a preview window. 22 | """ 23 | global ui_thread 24 | photo = Photo(filename=filename, ui_thread=ui_thread) 25 | if cache: 26 | global photos 27 | photos.append(photo) 28 | return photo 29 | 30 | 31 | if __name__ == '__main__': 32 | GLib.set_application_name(photorepl.app_name) 33 | 34 | import libraw 35 | 36 | import rawkit 37 | from rawkit.options import Options 38 | from rawkit.options import WhiteBalance 39 | from rawkit.raw import Raw 40 | 41 | # Launch the UI on start if we specify something to open. 42 | ui_thread = UIThread() 43 | ui_thread.start() 44 | 45 | if len(sys.argv) > 1: 46 | photos = [edit(arg, cache=False) for arg in sys.argv[1:]] 47 | else: 48 | photos = [] 49 | 50 | print(""" 51 | Good morning (UGT)! Welcome to photoREPL, an experimental interface for raw 52 | photo editing from the command line with `rawkit'. 53 | 54 | The following packages, modules, and classes are imported for you (among 55 | others): 56 | 57 | libraw 58 | 59 | photorepl 60 | photorepl.photo.Photo 61 | 62 | rawkit 63 | rawkit.options.Options 64 | rawkit.options.WhiteBalance 65 | rawkit.raw.Raw 66 | 67 | The following functions are also available: 68 | 69 | edit(filename) 70 | 71 | For help, use the `help()' function, eg. `help(Photo)'. 72 | """) 73 | 74 | if len(sys.argv) == 1: 75 | print(""" 76 | To get started, why not try opening a photo with: 77 | 78 | myphoto = edit(filename=somephoto) 79 | """) 80 | elif len(sys.argv) == 2: 81 | print("The file `{}' is available as photos[0].".format( 82 | sys.argv[1])) 83 | elif len(sys.argv) > 2: 84 | print("The files {} are available in the photos[] array.".format( 85 | sys.argv[1:])) 86 | 87 | @atexit.register 88 | def on_exit(): 89 | 90 | # Cleanup 91 | global photos 92 | for photo in photos: 93 | photo.close() 94 | 95 | print(""" 96 | Goodbye. If photoREPL immediately exited, be sure you're running 97 | photoREPL with `python -i -m photorepl' so that it can fall back to a 98 | prompt. 99 | """) 100 | -------------------------------------------------------------------------------- /photorepl/photo.py: -------------------------------------------------------------------------------- 1 | import os 2 | import tempfile 3 | import threading 4 | 5 | from gi.repository import Gdk 6 | 7 | from rawkit.options import Options 8 | from rawkit.raw import Raw 9 | 10 | 11 | class AutoUpdatingOptions(Options): 12 | 13 | """ 14 | A set of options that update the photo when they are updated. 15 | """ 16 | 17 | def __init__(self, attrs=None, photo=None): 18 | super().__init__(attrs=attrs) 19 | self.photo = photo 20 | 21 | def __setattr__(self, name, value): 22 | try: 23 | Options.__setattr__(self, name, value) 24 | self.update() 25 | except AttributeError: 26 | self.__dict__['name'] = value 27 | 28 | def update(self): 29 | """ 30 | Updates the photo which contains these options. 31 | """ 32 | 33 | if self.photo is not None: 34 | self.photo.update() 35 | 36 | 37 | class Photo(Raw): 38 | 39 | """ 40 | A photo comprises a raw file which can be edited and will update the 41 | associated preview window (if any). 42 | """ 43 | 44 | def __init__(self, filename=None, ui_thread=None): 45 | super().__init__(filename=filename) 46 | 47 | (self.fhandle, self.tempfile) = tempfile.mkstemp() 48 | self.filename = filename 49 | self.ui_thread = ui_thread 50 | self._closed = False 51 | 52 | if self.ui_thread is not None: 53 | self.update() 54 | self.show() 55 | 56 | def __setattr__(self, name, value): 57 | if name == 'options' and type(value) is Options: 58 | self.__dict__['options'] = AutoUpdatingOptions( 59 | attrs=dict(zip( 60 | value.keys(), 61 | value.values() 62 | )), 63 | photo=self 64 | ) 65 | try: 66 | self.update() 67 | except AttributeError: 68 | pass 69 | else: 70 | Raw.__setattr__(self, name, value) 71 | 72 | def show(self): 73 | """ 74 | Show the preview window. 75 | """ 76 | 77 | try: 78 | Gdk.threads_enter() 79 | self.preview = self.ui_thread.open_window( 80 | self.tempfile, 81 | rawfile=self.filename 82 | ) 83 | finally: 84 | Gdk.threads_leave() 85 | 86 | def _update(self): 87 | try: 88 | Gdk.threads_enter() 89 | self.save(filename=self.tempfile, filetype='ppm') 90 | self.preview.render_photo(filename=self.tempfile) 91 | except AttributeError: 92 | pass 93 | finally: 94 | Gdk.threads_leave() 95 | 96 | def update(self): 97 | """ 98 | Updates the photo on disk and in the preview pane. 99 | """ 100 | 101 | t = threading.Thread(target=self._update) 102 | t.daemon = True 103 | t.start() 104 | 105 | @property 106 | def closed(self): 107 | return self._closed 108 | 109 | def close(self): 110 | """ 111 | Cleans up the underlying raw file and unlinks any temp files. 112 | """ 113 | 114 | if not self.closed: 115 | super().close() 116 | os.unlink(self.tempfile) 117 | self.preview.close() 118 | self._closed = True 119 | --------------------------------------------------------------------------------