├── photobooth ├── gui │ ├── Qt5Gui │ │ ├── stylesheets │ │ │ ├── default.qss │ │ │ ├── dark-800x600.qss │ │ │ ├── dark-1024x600.qss │ │ │ ├── pastel-800x600.qss │ │ │ └── pastel-1024x600.qss │ │ ├── images │ │ │ ├── checkmark.png │ │ │ ├── arrow-800x600.png │ │ │ ├── arrow-1024x600.png │ │ │ ├── camera-1024x600.png │ │ │ ├── camera-800x600.png │ │ │ ├── checkmark.svg │ │ │ ├── camera.svg │ │ │ └── arrow.svg │ │ ├── fonts │ │ │ ├── AmaticSC-Bold.ttf │ │ │ ├── AmaticSC-Regular.ttf │ │ │ └── OFL.txt │ │ ├── __init__.py │ │ ├── Receiver.py │ │ ├── Worker.py │ │ └── Widgets.py │ ├── __init__.py │ ├── GuiSkeleton.py │ └── GuiPostprocessor.py ├── locale │ ├── de │ │ └── LC_MESSAGES │ │ │ ├── photobooth.mo │ │ │ └── photobooth.po │ ├── en │ │ └── LC_MESSAGES │ │ │ ├── photobooth.mo │ │ │ └── photobooth.po │ ├── es │ │ └── LC_MESSAGES │ │ │ ├── photobooth.mo │ │ │ └── photobooth.po │ ├── fr │ │ └── LC_MESSAGES │ │ │ ├── photobooth.mo │ │ │ └── photobooth.po │ └── messages.pot ├── camera │ ├── models │ │ ├── canoneos500d.cfg │ │ └── defaults.cfg │ ├── CameraDummy.py │ ├── CameraOpenCV.py │ ├── CameraGphoto2CommandLine.py │ ├── CameraPicamera.py │ ├── CameraInterface.py │ ├── CameraGphoto2Cffi.py │ ├── CameraGphoto2.py │ ├── PictureDimensions.py │ └── __init__.py ├── __main__.py ├── __init__.py ├── worker │ ├── WorkerTask.py │ ├── PictureSaver.py │ ├── PictureUploadWebdav.py │ ├── PictureList.py │ ├── PictureMailer.py │ └── __init__.py ├── printer │ ├── __init__.py │ ├── PrinterPyQt5.py │ └── PrinterPyCups.py ├── util.py ├── Threading.py ├── Config.py ├── defaults.cfg └── main.py ├── autostart.sh ├── screenshots ├── dark_1.png ├── dark_2.png ├── dark_3.png ├── dark_4.png ├── pastel_1.png ├── pastel_2.png ├── pastel_3.png ├── pastel_4.png └── pastel_settings.png ├── supplementals ├── wiring │ ├── wiring.fzz │ └── wiring_bb.png ├── housing │ ├── FFF Tusj.ttf │ ├── Cuttertemplate.pdf │ ├── Cuttertemplate.svg │ └── Displayframe.svg ├── backgrounds │ ├── halloween.jpg │ └── limestone.jpg ├── state-chart │ ├── state-chart.pdf │ └── state-chart.tex ├── pibakery │ ├── PiBakery-CreateImage.gif │ └── raspbian+photobooth+raspap.xml └── Canon_SELPHY_CP1300.ppd ├── MANIFEST.in ├── .gitignore ├── setup.cfg ├── .github └── ISSUE_TEMPLATE │ ├── success-story.md │ └── bug_report.md ├── README.md └── INSTALL.md /photobooth/gui/Qt5Gui/stylesheets/default.qss: -------------------------------------------------------------------------------- 1 | /* empty stylesheet: use system colors */ -------------------------------------------------------------------------------- /autostart.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | cd "$(dirname "$0")" 4 | .venv/bin/python -m photobooth 5 | 6 | -------------------------------------------------------------------------------- /screenshots/dark_1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/reuterbal/photobooth/HEAD/screenshots/dark_1.png -------------------------------------------------------------------------------- /screenshots/dark_2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/reuterbal/photobooth/HEAD/screenshots/dark_2.png -------------------------------------------------------------------------------- /screenshots/dark_3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/reuterbal/photobooth/HEAD/screenshots/dark_3.png -------------------------------------------------------------------------------- /screenshots/dark_4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/reuterbal/photobooth/HEAD/screenshots/dark_4.png -------------------------------------------------------------------------------- /screenshots/pastel_1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/reuterbal/photobooth/HEAD/screenshots/pastel_1.png -------------------------------------------------------------------------------- /screenshots/pastel_2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/reuterbal/photobooth/HEAD/screenshots/pastel_2.png -------------------------------------------------------------------------------- /screenshots/pastel_3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/reuterbal/photobooth/HEAD/screenshots/pastel_3.png -------------------------------------------------------------------------------- /screenshots/pastel_4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/reuterbal/photobooth/HEAD/screenshots/pastel_4.png -------------------------------------------------------------------------------- /screenshots/pastel_settings.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/reuterbal/photobooth/HEAD/screenshots/pastel_settings.png -------------------------------------------------------------------------------- /supplementals/wiring/wiring.fzz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/reuterbal/photobooth/HEAD/supplementals/wiring/wiring.fzz -------------------------------------------------------------------------------- /supplementals/housing/FFF Tusj.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/reuterbal/photobooth/HEAD/supplementals/housing/FFF Tusj.ttf -------------------------------------------------------------------------------- /supplementals/wiring/wiring_bb.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/reuterbal/photobooth/HEAD/supplementals/wiring/wiring_bb.png -------------------------------------------------------------------------------- /supplementals/backgrounds/halloween.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/reuterbal/photobooth/HEAD/supplementals/backgrounds/halloween.jpg -------------------------------------------------------------------------------- /supplementals/backgrounds/limestone.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/reuterbal/photobooth/HEAD/supplementals/backgrounds/limestone.jpg -------------------------------------------------------------------------------- /supplementals/housing/Cuttertemplate.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/reuterbal/photobooth/HEAD/supplementals/housing/Cuttertemplate.pdf -------------------------------------------------------------------------------- /supplementals/state-chart/state-chart.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/reuterbal/photobooth/HEAD/supplementals/state-chart/state-chart.pdf -------------------------------------------------------------------------------- /photobooth/gui/Qt5Gui/images/checkmark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/reuterbal/photobooth/HEAD/photobooth/gui/Qt5Gui/images/checkmark.png -------------------------------------------------------------------------------- /photobooth/gui/Qt5Gui/fonts/AmaticSC-Bold.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/reuterbal/photobooth/HEAD/photobooth/gui/Qt5Gui/fonts/AmaticSC-Bold.ttf -------------------------------------------------------------------------------- /photobooth/gui/Qt5Gui/images/arrow-800x600.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/reuterbal/photobooth/HEAD/photobooth/gui/Qt5Gui/images/arrow-800x600.png -------------------------------------------------------------------------------- /photobooth/locale/de/LC_MESSAGES/photobooth.mo: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/reuterbal/photobooth/HEAD/photobooth/locale/de/LC_MESSAGES/photobooth.mo -------------------------------------------------------------------------------- /photobooth/locale/en/LC_MESSAGES/photobooth.mo: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/reuterbal/photobooth/HEAD/photobooth/locale/en/LC_MESSAGES/photobooth.mo -------------------------------------------------------------------------------- /photobooth/locale/es/LC_MESSAGES/photobooth.mo: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/reuterbal/photobooth/HEAD/photobooth/locale/es/LC_MESSAGES/photobooth.mo -------------------------------------------------------------------------------- /photobooth/locale/fr/LC_MESSAGES/photobooth.mo: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/reuterbal/photobooth/HEAD/photobooth/locale/fr/LC_MESSAGES/photobooth.mo -------------------------------------------------------------------------------- /photobooth/gui/Qt5Gui/fonts/AmaticSC-Regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/reuterbal/photobooth/HEAD/photobooth/gui/Qt5Gui/fonts/AmaticSC-Regular.ttf -------------------------------------------------------------------------------- /photobooth/gui/Qt5Gui/images/arrow-1024x600.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/reuterbal/photobooth/HEAD/photobooth/gui/Qt5Gui/images/arrow-1024x600.png -------------------------------------------------------------------------------- /photobooth/gui/Qt5Gui/images/camera-1024x600.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/reuterbal/photobooth/HEAD/photobooth/gui/Qt5Gui/images/camera-1024x600.png -------------------------------------------------------------------------------- /photobooth/gui/Qt5Gui/images/camera-800x600.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/reuterbal/photobooth/HEAD/photobooth/gui/Qt5Gui/images/camera-800x600.png -------------------------------------------------------------------------------- /supplementals/pibakery/PiBakery-CreateImage.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/reuterbal/photobooth/HEAD/supplementals/pibakery/PiBakery-CreateImage.gif -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include *.md 2 | include LICENSE.txt 3 | recursive-include photobooth *.cfg 4 | recursive-include photobooth/locale *.po 5 | recursive-include photobooth/locale *.mo 6 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | photobooth.cfg 2 | **/*.pyc 3 | **/__pycache__ 4 | .eggs 5 | .venv 6 | photobooth.egg-info 7 | *.log 8 | *.log.20??-??-?? 9 | print_*.pdf 10 | 20??-??-?? 11 | build 12 | dist 13 | 14 | -------------------------------------------------------------------------------- /photobooth/camera/models/canoneos500d.cfg: -------------------------------------------------------------------------------- 1 | [Startup] 2 | imageformat = Large Fine JPEG 3 | imageformatsd = Large Fine JPEG 4 | autopoweroff = 0 5 | 6 | [Shutdown] 7 | imageformat = RAW 8 | imageformatsd = RAW 9 | autopoweroff = 30 10 | 11 | [Idle] 12 | output = Off 13 | 14 | [Active] 15 | output = PC -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [extract_messages] 2 | input_dirs = photobooth 3 | output_file = photobooth/locale/messages.pot 4 | 5 | [init_catalog] 6 | domain = photobooth 7 | input_file = photobooth/locale/messages.pot 8 | output_dir = photobooth/locale 9 | 10 | [update_catalog] 11 | domain = photobooth 12 | input_file = photobooth/locale/messages.pot 13 | output_dir = photobooth/locale 14 | 15 | [compile_catalog] 16 | domain = photobooth 17 | directory = photobooth/locale 18 | -------------------------------------------------------------------------------- /photobooth/camera/models/defaults.cfg: -------------------------------------------------------------------------------- 1 | [Startup] 2 | # Define here settings that should be applied on startup, e.g., make sure 3 | # the file format is set to JPEG 4 | # Settings should be specified in the form 5 | # settingname = value 6 | 7 | [Shutdown] 8 | # Define here settings that should be applied on shutdown, e.g., change 9 | # file format back to RAW 10 | 11 | [Idle] 12 | # Specify here the configuration key and value that has to be applied to 13 | # set the camera to idle 14 | 15 | [Active] 16 | # Specify here the configuration key and value that has to be applied to 17 | # make the camera active 18 | -------------------------------------------------------------------------------- /photobooth/__main__.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | 4 | # Photobooth - a flexible photo booth software 5 | # Copyright (C) 2018 Balthasar Reuter 6 | # 7 | # This program is free software: you can redistribute it and/or modify 8 | # it under the terms of the GNU Affero General Public License as published by 9 | # the Free Software Foundation, either version 3 of the License, or 10 | # (at your option) any later version. 11 | # 12 | # This program is distributed in the hope that it will be useful, 13 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 14 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 15 | # GNU Affero General Public License for more details. 16 | # 17 | # You should have received a copy of the GNU Affero General Public License 18 | # along with this program. If not, see . 19 | 20 | import sys 21 | import photobooth 22 | 23 | sys.exit(photobooth.main(sys.argv)) 24 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/success-story.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Success story 3 | about: Describe successful use of the photobooth application 4 | 5 | --- 6 | 7 | **Describe your photobooth** 8 | For what type of event have you been using the application? What does your photooboth look like? 9 | 10 | **Hardware (please complete the following information):** 11 | - Device [e.g. Intel Laptop, Raspberry Pi 3B+, Odroid C2] 12 | - Camera [e.g. Canon EOS 500D] 13 | - GPIO: [Yes/No] 14 | 15 | **Additional equipment** 16 | - Flash/Light 17 | - External USB battery 18 | - Switch 19 | - (Touch)Screen 20 | - ... 21 | 22 | **Software (please complete the following information):** 23 | - OS [e.g. Raspbian Stretch] 24 | - Python version [e.g. 3.5.1] 25 | 26 | **Modifications:** 27 | - List changes... 28 | - ...or added features 29 | 30 | **Problems:** 31 | - A list of problems... 32 | - ...that you experienced during the use 33 | 34 | **Pictures:** 35 | If you like, add pictures of your photobooth, include screenshots etc. 36 | -------------------------------------------------------------------------------- /photobooth/gui/__init__.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | 4 | # Photobooth - a flexible photo booth software 5 | # Copyright (C) 2018 Balthasar Reuter 6 | # 7 | # This program is free software: you can redistribute it and/or modify 8 | # it under the terms of the GNU Affero General Public License as published by 9 | # the Free Software Foundation, either version 3 of the License, or 10 | # (at your option) any later version. 11 | # 12 | # This program is distributed in the hope that it will be useful, 13 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 14 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 15 | # GNU Affero General Public License for more details. 16 | # 17 | # You should have received a copy of the GNU Affero General Public License 18 | # along with this program. If not, see . 19 | 20 | # Available gui modules as tuples of (config name, module name, class name) 21 | modules = (('PyQt5', 'Qt5Gui', 'PyQt5Gui'), ) 22 | -------------------------------------------------------------------------------- /photobooth/__init__.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | 4 | # Photobooth - a flexible photo booth software 5 | # Copyright (C) 2018 Balthasar Reuter 6 | # 7 | # This program is free software: you can redistribute it and/or modify 8 | # it under the terms of the GNU Affero General Public License as published by 9 | # the Free Software Foundation, either version 3 of the License, or 10 | # (at your option) any later version. 11 | # 12 | # This program is distributed in the hope that it will be useful, 13 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 14 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 15 | # GNU Affero General Public License for more details. 16 | # 17 | # You should have received a copy of the GNU Affero General Public License 18 | # along with this program. If not, see . 19 | 20 | import sys 21 | from .main import main 22 | 23 | name = "photobooth" 24 | 25 | if __name__ == "__main__": 26 | sys.exit(main(sys.argv)) 27 | -------------------------------------------------------------------------------- /photobooth/worker/WorkerTask.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | 4 | # Photobooth - a flexible photo booth software 5 | # Copyright (C) 2018 Balthasar Reuter 6 | # 7 | # This program is free software: you can redistribute it and/or modify 8 | # it under the terms of the GNU Affero General Public License as published by 9 | # the Free Software Foundation, either version 3 of the License, or 10 | # (at your option) any later version. 11 | # 12 | # This program is distributed in the hope that it will be useful, 13 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 14 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 15 | # GNU Affero General Public License for more details. 16 | # 17 | # You should have received a copy of the GNU Affero General Public License 18 | # along with this program. If not, see . 19 | 20 | 21 | class WorkerTask: 22 | 23 | def __init__(self, **kwargs): 24 | 25 | assert not kwargs 26 | 27 | def do(self, picture): 28 | 29 | raise NotImplementedError() 30 | -------------------------------------------------------------------------------- /photobooth/gui/Qt5Gui/__init__.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | 4 | # Photobooth - a flexible photo booth software 5 | # Copyright (C) 2018 Balthasar Reuter 6 | # 7 | # This program is free software: you can redistribute it and/or modify 8 | # it under the terms of the GNU Affero General Public License as published by 9 | # the Free Software Foundation, either version 3 of the License, or 10 | # (at your option) any later version. 11 | # 12 | # This program is distributed in the hope that it will be useful, 13 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 14 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 15 | # GNU Affero General Public License for more details. 16 | # 17 | # You should have received a copy of the GNU Affero General Public License 18 | # along with this program. If not, see . 19 | 20 | # Available style sheets as tuples of (style name, style file) 21 | styles = (('default', 'stylesheets/default.qss'), 22 | ('dark (1024 x 600 px)', 'stylesheets/dark-1024x600.qss'), 23 | ('dark (800 x 600 px)', 'stylesheets/dark-800x600.qss'), 24 | ('pastel (1024 x 600 px)', 'stylesheets/pastel-1024x600.qss'), 25 | ('pastel (800 x 600 px)', 'stylesheets/pastel-800x600.qss')) 26 | 27 | from .PyQt5Gui import PyQt5Gui # noqa 28 | -------------------------------------------------------------------------------- /photobooth/gui/Qt5Gui/Receiver.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | 4 | # Photobooth - a flexible photo booth software 5 | # Copyright (C) 2018 Balthasar Reuter 6 | # 7 | # This program is free software: you can redistribute it and/or modify 8 | # it under the terms of the GNU Affero General Public License as published by 9 | # the Free Software Foundation, either version 3 of the License, or 10 | # (at your option) any later version. 11 | # 12 | # This program is distributed in the hope that it will be useful, 13 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 14 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 15 | # GNU Affero General Public License for more details. 16 | # 17 | # You should have received a copy of the GNU Affero General Public License 18 | # along with this program. If not, see . 19 | 20 | from PyQt5 import QtCore 21 | 22 | from ...Threading import Workers 23 | 24 | 25 | class Receiver(QtCore.QThread): 26 | 27 | notify = QtCore.pyqtSignal(object) 28 | 29 | def __init__(self, comm): 30 | 31 | super().__init__() 32 | self._comm = comm 33 | 34 | def handle(self, state): 35 | 36 | self.notify.emit(state) 37 | 38 | def run(self): 39 | 40 | for state in self._comm.iter(Workers.GUI): 41 | self.handle(state) 42 | -------------------------------------------------------------------------------- /photobooth/worker/PictureSaver.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | 4 | # Photobooth - a flexible photo booth software 5 | # Copyright (C) 2018 Balthasar Reuter 6 | # 7 | # This program is free software: you can redistribute it and/or modify 8 | # it under the terms of the GNU Affero General Public License as published by 9 | # the Free Software Foundation, either version 3 of the License, or 10 | # (at your option) any later version. 11 | # 12 | # This program is distributed in the hope that it will be useful, 13 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 14 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 15 | # GNU Affero General Public License for more details. 16 | # 17 | # You should have received a copy of the GNU Affero General Public License 18 | # along with this program. If not, see . 19 | 20 | import logging 21 | import os 22 | 23 | from .WorkerTask import WorkerTask 24 | 25 | 26 | class PictureSaver(WorkerTask): 27 | 28 | def __init__(self, basename): 29 | 30 | super().__init__() 31 | 32 | # Ensure directory exists 33 | dirname = os.path.dirname(basename) 34 | if not os.path.exists(dirname): 35 | os.makedirs(dirname) 36 | 37 | def do(self, picture, filename): 38 | 39 | logging.info('Saving picture as %s', filename) 40 | with open(filename, 'wb') as f: 41 | f.write(picture.getbuffer()) 42 | -------------------------------------------------------------------------------- /photobooth/gui/Qt5Gui/Worker.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | 4 | # Photobooth - a flexible photo booth software 5 | # Copyright (C) 2018 Balthasar Reuter 6 | # 7 | # This program is free software: you can redistribute it and/or modify 8 | # it under the terms of the GNU Affero General Public License as published by 9 | # the Free Software Foundation, either version 3 of the License, or 10 | # (at your option) any later version. 11 | # 12 | # This program is distributed in the hope that it will be useful, 13 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 14 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 15 | # GNU Affero General Public License for more details. 16 | # 17 | # You should have received a copy of the GNU Affero General Public License 18 | # along with this program. If not, see . 19 | 20 | import queue 21 | 22 | from PyQt5 import QtCore 23 | 24 | 25 | class Worker(QtCore.QThread): 26 | 27 | def __init__(self, comm): 28 | 29 | super().__init__() 30 | self._comm = comm 31 | self._queue = queue.Queue() 32 | 33 | def put(self, task): 34 | 35 | self._queue.put(task) 36 | 37 | def get(self): 38 | 39 | return self._queue.get() 40 | 41 | def done(self): 42 | 43 | self._queue.task_done() 44 | 45 | def run(self): 46 | 47 | while True: 48 | task = self.get() 49 | if task is None: 50 | break 51 | task() 52 | self.done() 53 | -------------------------------------------------------------------------------- /photobooth/printer/__init__.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | 4 | # Photobooth - a flexible photo booth software 5 | # Copyright (C) 2018 Balthasar Reuter 6 | # 7 | # This program is free software: you can redistribute it and/or modify 8 | # it under the terms of the GNU Affero General Public License as published by 9 | # the Free Software Foundation, either version 3 of the License, or 10 | # (at your option) any later version. 11 | # 12 | # This program is distributed in the hope that it will be useful, 13 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 14 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 15 | # GNU Affero General Public License for more details. 16 | # 17 | # You should have received a copy of the GNU Affero General Public License 18 | # along with this program. If not, see . 19 | 20 | # Available printer modules as tuples of (config name, module name, class name) 21 | modules = ( 22 | ('PyQt5', 'PrinterPyQt5', 'PrinterPyQt5'), 23 | ('PyCUPS', 'PrinterPyCups', 'PrinterPyCups')) 24 | 25 | 26 | class Printer: 27 | 28 | def __init__(self, page_size): 29 | 30 | self.pageSize = page_size 31 | 32 | @property 33 | def pageSize(self): 34 | 35 | return self._page_size 36 | 37 | @pageSize.setter 38 | def pageSize(self, page_size): 39 | 40 | if not isinstance(page_size, (list, tuple)) or len(page_size) != 2: 41 | raise ValueError('page_size must be a list/tuple of length 2') 42 | 43 | self._page_size = page_size 44 | 45 | def print(self, picture): 46 | 47 | raise NotImplementedError('print function not implemented!') 48 | -------------------------------------------------------------------------------- /photobooth/camera/CameraDummy.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | 4 | # Photobooth - a flexible photo booth software 5 | # Copyright (C) 2018 Balthasar Reuter 6 | # 7 | # This program is free software: you can redistribute it and/or modify 8 | # it under the terms of the GNU Affero General Public License as published by 9 | # the Free Software Foundation, either version 3 of the License, or 10 | # (at your option) any later version. 11 | # 12 | # This program is distributed in the hope that it will be useful, 13 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 14 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 15 | # GNU Affero General Public License for more details. 16 | # 17 | # You should have received a copy of the GNU Affero General Public License 18 | # along with this program. If not, see . 19 | 20 | import logging 21 | from colorsys import hsv_to_rgb 22 | 23 | from PIL import Image 24 | 25 | from .CameraInterface import CameraInterface 26 | 27 | 28 | class CameraDummy(CameraInterface): 29 | 30 | def __init__(self): 31 | 32 | super().__init__() 33 | 34 | self.hasPreview = True 35 | self.hasIdle = False 36 | self._size = (1920, 1280) 37 | 38 | self._hue = 0 39 | 40 | logging.info('Using CameraDummy') 41 | 42 | def getPreview(self): 43 | 44 | return self.getPicture() 45 | 46 | def getPicture(self): 47 | 48 | self._hue = (self._hue + 1) % 360 49 | r, g, b = hsv_to_rgb(self._hue / 360, .2, .9) 50 | return Image.new('RGB', self._size, (int(r * 255), int(g * 255), 51 | int(b * 255))) 52 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | 5 | --- 6 | 7 | **Describe the bug** 8 | 9 | A clear and concise description of what the bug is. 10 | 11 | **To Reproduce** 12 | 13 | Steps to reproduce the behavior: 14 | 1. Go to '...' 15 | 2. Click on '....' 16 | 3. Scroll down to '....' 17 | 4. See error 18 | 19 | **Expected behavior** 20 | 21 | A clear and concise description of what you expected to happen. 22 | 23 | **Screenshots/Screencast** 24 | 25 | If applicable, add screenshots to help explain your problem. 26 | 27 | OR 28 | 29 | Attach a session recording using [asciinema](https://asciinema.org/). 30 | To use it: 31 | ```bash 32 | sudo apt install -y asciinema && asciinema rec # start session recording 33 | ``` 34 | Reproduce the issue: 35 | ```bash 36 | pip install -e .[...] # Install command if you encounter issues during install process 37 | ``` 38 | OR 39 | ```bash 40 | .venv/bin/python -m photobooth # To start photobooth if you encounter issue during usage 41 | ``` 42 | Stop recording and upload: 43 | ``` 44 | Ctrl+D # stop recording 45 | y # yes to upload and get URL to paste here 46 | ``` 47 | 48 | **Hardware (please complete the following information)** 49 | 50 | - Device [e.g. Intel Laptop, Raspberry Pi 3B+, Odroid C2] 51 | - Camera [e.g. Canon EOS 500D] 52 | - GPIO: [Yes/No] 53 | 54 | **Software (please complete the following information)** 55 | 56 | - OS [e.g. Raspbian Stretch] 57 | - Python version [e.g. 3.5.1] 58 | 59 | **Installed packages** 60 | 61 | ``` 62 | Run 'pip freeze' in your virtual environment and paste the output here 63 | ``` 64 | 65 | **Photobooth log** 66 | 67 | Run the application as `.venv/bin/python -m photobooth --debug` and paste the logfile here: 68 | 69 | ``` 70 | Insert the content of photobooth.log here 71 | ``` 72 | 73 | **Additional context** 74 | 75 | Add any other context about the problem here. 76 | -------------------------------------------------------------------------------- /photobooth/worker/PictureUploadWebdav.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | 4 | # Photobooth - a flexible photo booth software 5 | # Copyright (C) 2018 Balthasar Reuter 6 | # 7 | # This program is free software: you can redistribute it and/or modify 8 | # it under the terms of the GNU Affero General Public License as published by 9 | # the Free Software Foundation, either version 3 of the License, or 10 | # (at your option) any later version. 11 | # 12 | # This program is distributed in the hope that it will be useful, 13 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 14 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 15 | # GNU Affero General Public License for more details. 16 | # 17 | # You should have received a copy of the GNU Affero General Public License 18 | # along with this program. If not, see . 19 | 20 | import logging 21 | import requests 22 | 23 | from pathlib import Path 24 | 25 | from .WorkerTask import WorkerTask 26 | 27 | 28 | class PictureUploadWebdav(WorkerTask): 29 | 30 | def __init__(self, config): 31 | 32 | super().__init__() 33 | 34 | self._baseurl = config.get('UploadWebdav', 'url') 35 | if config.getBool('UploadWebdav', 'use_auth'): 36 | self._auth = (config.get('UploadWebdav', 'user'), 37 | config.get('UploadWebdav', 'password')) 38 | else: 39 | self._auth = None 40 | 41 | def do(self, picture, filename): 42 | 43 | url = self._baseurl + '/' + Path(filename).name 44 | logging.info('Uploading picture as %s', url) 45 | 46 | r = requests.put(url, data=picture.getbuffer(), auth=self._auth) 47 | if r.status_code in range(200, 300): 48 | logging.warn(('PictureUploadWebdav: Upload failed with ' 49 | 'status code {}').format(r.status_code)) 50 | -------------------------------------------------------------------------------- /photobooth/util.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | 4 | # Photobooth - a flexible photo booth software 5 | # Copyright (C) 2018 Balthasar Reuter 6 | # 7 | # This program is free software: you can redistribute it and/or modify 8 | # it under the terms of the GNU Affero General Public License as published by 9 | # the Free Software Foundation, either version 3 of the License, or 10 | # (at your option) any later version. 11 | # 12 | # This program is distributed in the hope that it will be useful, 13 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 14 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 15 | # GNU Affero General Public License for more details. 16 | # 17 | # You should have received a copy of the GNU Affero General Public License 18 | # along with this program. If not, see . 19 | 20 | import importlib 21 | 22 | from PIL import Image 23 | 24 | 25 | def lookup_and_import(module_list, name, package=None): 26 | 27 | result = next(((mod_name, class_name) 28 | for config_name, mod_name, class_name in module_list 29 | if name == config_name), None) 30 | 31 | if package is None: 32 | import_module = importlib.import_module('photobooth.' + result[0]) 33 | else: 34 | import_module = importlib.import_module( 35 | 'photobooth.' + package + '.' + result[0]) 36 | 37 | if result[1] is None: 38 | return import_module 39 | else: 40 | return getattr(import_module, result[1]) 41 | 42 | 43 | def pickle_image(image): 44 | 45 | if image is None: 46 | return None 47 | else: 48 | image_data = (image.mode, image.size, image.tobytes()) 49 | return image_data 50 | 51 | 52 | def unpickle_image(image_data): 53 | 54 | if image_data is None: 55 | return None 56 | else: 57 | image = Image.frombytes(*image_data) 58 | return image 59 | -------------------------------------------------------------------------------- /photobooth/camera/CameraOpenCV.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | 4 | # Photobooth - a flexible photo booth software 5 | # Copyright (C) 2018 Balthasar Reuter 6 | # 7 | # This program is free software: you can redistribute it and/or modify 8 | # it under the terms of the GNU Affero General Public License as published by 9 | # the Free Software Foundation, either version 3 of the License, or 10 | # (at your option) any later version. 11 | # 12 | # This program is distributed in the hope that it will be useful, 13 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 14 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 15 | # GNU Affero General Public License for more details. 16 | # 17 | # You should have received a copy of the GNU Affero General Public License 18 | # along with this program. If not, see . 19 | 20 | import logging 21 | 22 | from PIL import Image 23 | 24 | import cv2 25 | 26 | from .CameraInterface import CameraInterface 27 | 28 | 29 | class CameraOpenCV(CameraInterface): 30 | 31 | def __init__(self): 32 | 33 | super().__init__() 34 | 35 | self.hasPreview = True 36 | self.hasIdle = True 37 | 38 | logging.info('Using OpenCV') 39 | 40 | self._cap = cv2.VideoCapture() 41 | 42 | def setActive(self): 43 | 44 | if not self._cap.isOpened(): 45 | self._cap.open(0) 46 | if not self._cap.isOpened(): 47 | raise RuntimeError('Camera could not be opened') 48 | 49 | def setIdle(self): 50 | 51 | if self._cap.isOpened(): 52 | self._cap.release() 53 | 54 | def getPreview(self): 55 | 56 | return self.getPicture() 57 | 58 | def getPicture(self): 59 | 60 | self.setActive() 61 | status, frame = self._cap.read() 62 | if not status: 63 | raise RuntimeError('Failed to capture picture') 64 | 65 | # OpenCV yields frames in BGR format, conversion to RGB necessary. 66 | # (See https://stackoverflow.com/a/32270308) 67 | return Image.fromarray(cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)) 68 | -------------------------------------------------------------------------------- /photobooth/camera/CameraGphoto2CommandLine.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | 4 | # Photobooth - a flexible photo booth software 5 | # Copyright (C) 2018 Balthasar Reuter 6 | # 7 | # This program is free software: you can redistribute it and/or modify 8 | # it under the terms of the GNU Affero General Public License as published by 9 | # the Free Software Foundation, either version 3 of the License, or 10 | # (at your option) any later version. 11 | # 12 | # This program is distributed in the hope that it will be useful, 13 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 14 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 15 | # GNU Affero General Public License for more details. 16 | # 17 | # You should have received a copy of the GNU Affero General Public License 18 | # along with this program. If not, see . 19 | 20 | import logging 21 | import os 22 | import subprocess 23 | 24 | from PIL import Image 25 | 26 | from .CameraInterface import CameraInterface 27 | 28 | 29 | class CameraGphoto2CommandLine(CameraInterface): 30 | 31 | def __init__(self): 32 | 33 | super().__init__() 34 | 35 | self.hasPreview = False 36 | self.hasIdle = False 37 | 38 | logging.info('Using gphoto2 via command line') 39 | 40 | if os.access('/dev/shm', os.W_OK): 41 | logging.debug('Storing temp files to "/dev/shm/photobooth.jpg"') 42 | self._tmp_filename = '/dev/shm/photobooth.jpg' 43 | else: 44 | logging.debug('Storing temp files to "/tmp/photobooth.jpg"') 45 | self._tmp_filename = '/tmp/photobooth.jpg' 46 | 47 | self.setActive() 48 | 49 | def setActive(self): 50 | 51 | self._callGphoto('-a', '/dev/null') 52 | 53 | def getPicture(self): 54 | 55 | self._callGphoto('--capture-image-and-download', self._tmp_filename) 56 | return Image.open(self._tmp_filename) 57 | 58 | def _callGphoto(self, action, filename): 59 | 60 | cmd = 'gphoto2 --force-overwrite --quiet {} --filename {}' 61 | return subprocess.check_output(cmd.format(action, filename), 62 | shell=True, stderr=subprocess.STDOUT) 63 | -------------------------------------------------------------------------------- /photobooth/Threading.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | 4 | # Photobooth - a flexible photo booth software 5 | # Copyright (C) 2018 Balthasar Reuter 6 | # 7 | # This program is free software: you can redistribute it and/or modify 8 | # it under the terms of the GNU Affero General Public License as published by 9 | # the Free Software Foundation, either version 3 of the License, or 10 | # (at your option) any later version. 11 | # 12 | # This program is distributed in the hope that it will be useful, 13 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 14 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 15 | # GNU Affero General Public License for more details. 16 | # 17 | # You should have received a copy of the GNU Affero General Public License 18 | # along with this program. If not, see . 19 | 20 | from enum import IntEnum 21 | from multiprocessing import Queue 22 | 23 | 24 | class Communicator: 25 | 26 | def __init__(self): 27 | 28 | super().__init__() 29 | 30 | self._queues = [Queue() for _ in Workers] 31 | 32 | def bcast(self, message): 33 | 34 | for q in self._queues[1:]: 35 | q.put(message) 36 | 37 | def send(self, target, message): 38 | 39 | if not isinstance(target, Workers): 40 | raise TypeError('target must be a member of Workers') 41 | 42 | self._queues[target].put(message) 43 | 44 | def recv(self, worker, block=True): 45 | 46 | if not isinstance(worker, Workers): 47 | raise TypeError('worker must be a member of Workers') 48 | 49 | return self._queues[worker].get(block) 50 | 51 | def iter(self, worker): 52 | 53 | if not isinstance(worker, Workers): 54 | raise TypeError('worker must be a member of Workers') 55 | 56 | return iter(self._queues[worker].get, None) 57 | 58 | def empty(self, worker): 59 | 60 | if not isinstance(worker, Workers): 61 | raise TypeError('worker must be a member of Workers') 62 | 63 | return self._queues[worker].empty() 64 | 65 | 66 | class Workers(IntEnum): 67 | 68 | MASTER = 0 69 | GUI = 1 70 | CAMERA = 2 71 | GPIO = 3 72 | WORKER = 4 73 | -------------------------------------------------------------------------------- /photobooth/camera/CameraPicamera.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | 4 | # Photobooth - a flexible photo booth software 5 | # Copyright (C) 2018 Balthasar Reuter 6 | # 7 | # This program is free software: you can redistribute it and/or modify 8 | # it under the terms of the GNU Affero General Public License as published by 9 | # the Free Software Foundation, either version 3 of the License, or 10 | # (at your option) any later version. 11 | # 12 | # This program is distributed in the hope that it will be useful, 13 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 14 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 15 | # GNU Affero General Public License for more details. 16 | # 17 | # You should have received a copy of the GNU Affero General Public License 18 | # along with this program. If not, see . 19 | 20 | import io 21 | import logging 22 | 23 | from PIL import Image 24 | 25 | from picamera import PiCamera 26 | 27 | from .CameraInterface import CameraInterface 28 | 29 | 30 | class CameraPicamera(CameraInterface): 31 | 32 | def __init__(self): 33 | 34 | super().__init__() 35 | 36 | self.hasPreview = True 37 | self.hasIdle = True 38 | 39 | logging.info('Using PiCamera') 40 | 41 | self._cap = None 42 | 43 | self.setActive() 44 | self._preview_resolution = (self._cap.resolution[0] // 2, 45 | self._cap.resolution[1] // 2) 46 | self.setIdle() 47 | 48 | def setActive(self): 49 | 50 | if self._cap is None or self._cap.closed: 51 | self._cap = PiCamera() 52 | 53 | def setIdle(self): 54 | 55 | if self._cap is not None and not self._cap.closed: 56 | self._cap.close() 57 | self._cap = None 58 | 59 | def getPreview(self): 60 | 61 | self.setActive() 62 | stream = io.BytesIO() 63 | self._cap.capture(stream, format='jpeg', use_video_port=True, 64 | resize=self._preview_resolution) 65 | stream.seek(0) 66 | return Image.open(stream) 67 | 68 | def getPicture(self): 69 | 70 | self.setActive() 71 | stream = io.BytesIO() 72 | self._cap.capture(stream, format='jpeg', resize=None) 73 | stream.seek(0) 74 | return Image.open(stream) 75 | -------------------------------------------------------------------------------- /photobooth/printer/PrinterPyQt5.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | 4 | # Photobooth - a flexible photo booth software 5 | # Copyright (C) 2018 Balthasar Reuter 6 | # 7 | # This program is free software: you can redistribute it and/or modify 8 | # it under the terms of the GNU Affero General Public License as published by 9 | # the Free Software Foundation, either version 3 of the License, or 10 | # (at your option) any later version. 11 | # 12 | # This program is distributed in the hope that it will be useful, 13 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 14 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 15 | # GNU Affero General Public License for more details. 16 | # 17 | # You should have received a copy of the GNU Affero General Public License 18 | # along with this program. If not, see . 19 | 20 | import logging 21 | 22 | from PyQt5 import QtCore, QtGui 23 | from PyQt5.QtPrintSupport import QPrinter 24 | 25 | from . import Printer 26 | 27 | 28 | class PrinterPyQt5(Printer): 29 | 30 | def __init__(self, page_size, print_pdf=False): 31 | 32 | super().__init__(page_size) 33 | 34 | self._printer = QPrinter(QPrinter.HighResolution) 35 | self._printer.setFullPage(True) 36 | self._printer.setPageSize(QtGui.QPageSize(QtCore.QSizeF(*page_size), 37 | QtGui.QPageSize.Millimeter)) 38 | self._printer.setColorMode(QPrinter.Color) 39 | 40 | logging.info('Using printer "%s"', self._printer.printerName()) 41 | 42 | self._print_pdf = print_pdf 43 | if self._print_pdf: 44 | logging.info('Using PDF printer') 45 | self._counter = 0 46 | self._printer.setOutputFormat(QPrinter.PdfFormat) 47 | 48 | def print(self, picture): 49 | 50 | if self._print_pdf: 51 | self._printer.setOutputFileName('print_%d.pdf' % self._counter) 52 | self._counter += 1 53 | 54 | logging.info('Printing picture') 55 | logging.debug('Page Size: {}, Print Size: {}, PictureSize: {} '.format( 56 | self._printer.paperRect(), self._printer.pageRect(), 57 | picture.rect())) 58 | 59 | painter = QtGui.QPainter(self._printer) 60 | painter.drawImage(self._printer.pageRect(), picture, picture.rect()) 61 | painter.end() 62 | -------------------------------------------------------------------------------- /photobooth/printer/PrinterPyCups.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | 4 | # Photobooth - a flexible photo booth software 5 | # Copyright (C) 2019 Balthasar Reuter 6 | # 7 | # This program is free software: you can redistribute it and/or modify 8 | # it under the terms of the GNU Affero General Public License as published by 9 | # the Free Software Foundation, either version 3 of the License, or 10 | # (at your option) any later version. 11 | # 12 | # This program is distributed in the hope that it will be useful, 13 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 14 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 15 | # GNU Affero General Public License for more details. 16 | # 17 | # You should have received a copy of the GNU Affero General Public License 18 | # along with this program. If not, see . 19 | # 20 | # This class was contributed by 21 | # @oelegeirnaert (https://github.com/oelegeirnaert) 22 | # see https://github.com/reuterbal/photobooth/pull/113 23 | 24 | import logging 25 | import os 26 | 27 | try: 28 | import cups 29 | except ImportError: 30 | logging.error('pycups is not installed') 31 | cups = None 32 | 33 | from PIL import ImageQt 34 | 35 | from . import Printer 36 | 37 | 38 | class PrinterPyCups(Printer): 39 | 40 | def __init__(self, page_size, print_pdf=False): 41 | 42 | self._conn = cups.Connection() if cups else None 43 | 44 | if print_pdf: 45 | logging.error('Printing to PDF not supported with pycups') 46 | self._conn = None 47 | 48 | if os.access('/dev/shm', os.W_OK): 49 | self._tmp_filename = '/dev/shm/print.jpg' 50 | else: 51 | self._tmp_filename = '/tmp/print.jpg' 52 | logging.debug('Storing temp files to "{}"'.format(self._tmp_filename)) 53 | 54 | if self._conn is not None: 55 | self._printer = self._conn.getDefault() 56 | logging.info('Using printer "%s"', self._printer) 57 | 58 | def print(self, picture): 59 | 60 | if self._conn is not None: 61 | if isinstance(picture, ImageQt.ImageQt): 62 | picture.save(self._tmp_filename) 63 | else: 64 | picture.save(self._tmp_filename, format='JPEG') 65 | self._conn.printFile(self._printer, self._tmp_filename, 66 | "photobooth", {}) 67 | -------------------------------------------------------------------------------- /photobooth/Config.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | 4 | # Photobooth - a flexible photo booth software 5 | # Copyright (C) 2018 Balthasar Reuter 6 | # 7 | # This program is free software: you can redistribute it and/or modify 8 | # it under the terms of the GNU Affero General Public License as published by 9 | # the Free Software Foundation, either version 3 of the License, or 10 | # (at your option) any later version. 11 | # 12 | # This program is distributed in the hope that it will be useful, 13 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 14 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 15 | # GNU Affero General Public License for more details. 16 | # 17 | # You should have received a copy of the GNU Affero General Public License 18 | # along with this program. If not, see . 19 | 20 | import configparser 21 | import logging 22 | import os 23 | 24 | 25 | class Config: 26 | 27 | def __init__(self, filename): 28 | 29 | self._filename = filename 30 | 31 | self._cfg = configparser.ConfigParser(interpolation=None) 32 | self.defaults() 33 | self.read() 34 | 35 | @property 36 | def filename(self): 37 | 38 | return self._filename 39 | 40 | @filename.setter 41 | def filename(self, value): 42 | 43 | self._filename = value 44 | 45 | def defaults(self): 46 | 47 | filename = os.path.join(os.path.dirname(__file__), 'defaults.cfg') 48 | logging.info('Reading config file "%s"', filename) 49 | self._cfg.read(filename) 50 | 51 | def read(self): 52 | 53 | logging.info('Reading config file "%s"', self._filename) 54 | self._cfg.read(self._filename) 55 | 56 | def write(self): 57 | 58 | logging.info('Writing config file "%s"', self._filename) 59 | with open(self._filename, 'w') as configfile: 60 | self._cfg.write(configfile) 61 | 62 | def get(self, section, key): 63 | 64 | return self._cfg[section][key] 65 | 66 | def getInt(self, section, key): 67 | 68 | return self._cfg.getint(section, key) 69 | 70 | def getFloat(self, section, key): 71 | 72 | return self._cfg.getfloat(section, key) 73 | 74 | def getBool(self, section, key): 75 | 76 | return self._cfg.getboolean(section, key) 77 | 78 | def getIntList(self, section, key): 79 | 80 | if len(self._cfg[section][key].strip()) > 0: 81 | return [int(i) for i in self._cfg[section][key].split(',')] 82 | else: 83 | return [] 84 | 85 | def set(self, section, key, value): 86 | 87 | self._cfg[section][key] = value 88 | -------------------------------------------------------------------------------- /photobooth/worker/PictureList.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | 4 | # Photobooth - a flexible photo booth software 5 | # Copyright (C) 2018 Balthasar Reuter 6 | # 7 | # This program is free software: you can redistribute it and/or modify 8 | # it under the terms of the GNU Affero General Public License as published by 9 | # the Free Software Foundation, either version 3 of the License, or 10 | # (at your option) any later version. 11 | # 12 | # This program is distributed in the hope that it will be useful, 13 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 14 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 15 | # GNU Affero General Public License for more details. 16 | # 17 | # You should have received a copy of the GNU Affero General Public License 18 | # along with this program. If not, see . 19 | 20 | import logging 21 | 22 | from glob import glob 23 | 24 | 25 | class PictureList: 26 | """A simple helper class. 27 | 28 | It provides the filenames for the assembled pictures and keeps count 29 | of taken and previously existing pictures. 30 | """ 31 | 32 | def __init__(self, basename): 33 | """Initialize filenames to the given basename and search for 34 | existing files. Set the counter accordingly. 35 | """ 36 | 37 | # Set basename and suffix 38 | self._basename = basename 39 | self.suffix = '.jpg' 40 | self.count_width = 5 41 | 42 | self.findExistingFiles() 43 | 44 | def findExistingFiles(self): 45 | """Count number of existing files matchin the given basename 46 | """ 47 | # Find existing files 48 | count_pattern = '[0-9]' * self.count_width 49 | pictures = glob(self.basename + count_pattern + self.suffix) 50 | 51 | # Get number of latest file 52 | if len(pictures) == 0: 53 | self.counter = 0 54 | else: 55 | pictures.sort() 56 | last_picture = pictures[-1] 57 | self.counter = int(last_picture[ 58 | -(self.count_width + len(self.suffix)):-len(self.suffix)]) 59 | 60 | # Print initial infos 61 | logging.info('Number of last existing file: %d', self.counter) 62 | logging.info('Saving pictures as "%s%s.%s"', self.basename, 63 | self.count_width * 'X', 'jpg') 64 | 65 | @property 66 | def basename(self): 67 | """Return the basename for the files""" 68 | return self._basename 69 | 70 | def getFilename(self, count): 71 | """Return the file name for a given file number""" 72 | return self.basename + str(count).zfill(self.count_width) + self.suffix 73 | 74 | def getLast(self): 75 | """Return the current filename""" 76 | return self.getFilename(self.counter) 77 | 78 | def getNext(self): 79 | """Update counter and return the next filename""" 80 | self.counter += 1 81 | return self.getFilename(self.counter) 82 | -------------------------------------------------------------------------------- /supplementals/pibakery/raspbian+photobooth+raspap.xml: -------------------------------------------------------------------------------- 1 | Desktopphotoboothraspberrysudo rpi-updatepisudo apt updatepisudo apt dist-upgrade -ypisudo sed -i 's/#xserver-command=X/xserver-command=X -s 0 -dpms/g' /etc/lightdm/lightdm.confpisudo apt install git python3-dev python3-pip virtualenv qt5-default pyqt5-dev pyqt5-dev-tools gphoto2 libgphoto2-dev -y pi/usr/share/dbus-1/services/org.gtk.vfs.GPhoto2VolumeMonitor.service/usr/share/gvfs/mounts/gphoto2.mount/usr/share/gvfs/remote-volume-monitors/gphoto2.mount/usr/lib/gvfs/gvfs-gphoto2-volume-monitor/usr/lib/gvfs/gvfsd-gphoto2git clone https://github.com/reuterbal/photobooth.gitpicd /home/pi/photobooth/pivirtualenv -p python3 --system-site-packages .venvpisource .venv/bin/activatepipip install -e .pisudo wget -q https://git.io/voEUQ -O /tmp/raspap && bash /tmp/raspappi/home/pi/photobooth/.venv/bin/python -m photobooth --runpi -------------------------------------------------------------------------------- /photobooth/camera/CameraInterface.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | 4 | # Photobooth - a flexible photo booth software 5 | # Copyright (C) 2018 Balthasar Reuter 6 | # 7 | # This program is free software: you can redistribute it and/or modify 8 | # it under the terms of the GNU Affero General Public License as published by 9 | # the Free Software Foundation, either version 3 of the License, or 10 | # (at your option) any later version. 11 | # 12 | # This program is distributed in the hope that it will be useful, 13 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 14 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 15 | # GNU Affero General Public License for more details. 16 | # 17 | # You should have received a copy of the GNU Affero General Public License 18 | # along with this program. If not, see . 19 | 20 | import configparser 21 | import logging 22 | import os 23 | 24 | 25 | class CameraInterface: 26 | 27 | def __init__(self): 28 | 29 | self.hasPreview = False 30 | self.hasIdle = False 31 | self._initConfig() 32 | 33 | def __enter__(self): 34 | 35 | return self 36 | 37 | def __exit__(self, exc_type, exc_value, traceback): 38 | 39 | self.cleanup() 40 | 41 | def cleanup(self): 42 | 43 | pass 44 | 45 | @property 46 | def hasPreview(self): 47 | 48 | return self._has_preview 49 | 50 | @hasPreview.setter 51 | def hasPreview(self, value): 52 | 53 | if not isinstance(value, bool): 54 | raise ValueError('Expected bool') 55 | 56 | self._has_preview = value 57 | 58 | @property 59 | def hasIdle(self): 60 | 61 | return self._has_idle 62 | 63 | @hasIdle.setter 64 | def hasIdle(self, value): 65 | 66 | if not isinstance(value, bool): 67 | raise ValueError('Expected bool') 68 | 69 | self._has_idle = value 70 | 71 | @property 72 | def config(self): 73 | return self._cfg 74 | 75 | def setActive(self): 76 | 77 | if not self.hasIdle: 78 | pass 79 | else: 80 | raise NotImplementedError() 81 | 82 | def setIdle(self): 83 | 84 | if not self.hasIdle: 85 | raise RuntimeError('Camera does not have idle functionality') 86 | 87 | raise NotImplementedError() 88 | 89 | def getPreview(self): 90 | 91 | if not self.hasPreview: 92 | raise RuntimeError('Camera does not have preview functionality') 93 | 94 | raise NotImplementedError() 95 | 96 | def getPicture(self): 97 | 98 | raise NotImplementedError() 99 | 100 | def _initConfig(self): 101 | 102 | self._cfg = configparser.ConfigParser(interpolation=None) 103 | filename = os.path.join(os.path.dirname(__file__), 'models', 104 | 'defaults.cfg') 105 | self._cfg.read(filename) 106 | 107 | def loadConfig(self, model): 108 | 109 | name = ''.join(c for c in model.lower() if c.isalnum()) + '.cfg' 110 | filename = os.path.join(os.path.dirname(__file__), 'models', name) 111 | logging.info('Loading camera config "{}"'.format(name)) 112 | self._cfg.read(filename) 113 | -------------------------------------------------------------------------------- /photobooth/gui/GuiSkeleton.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | 4 | # Photobooth - a flexible photo booth software 5 | # Copyright (C) 2018 Balthasar Reuter 6 | # 7 | # This program is free software: you can redistribute it and/or modify 8 | # it under the terms of the GNU Affero General Public License as published by 9 | # the Free Software Foundation, either version 3 of the License, or 10 | # (at your option) any later version. 11 | # 12 | # This program is distributed in the hope that it will be useful, 13 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 14 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 15 | # GNU Affero General Public License for more details. 16 | # 17 | # You should have received a copy of the GNU Affero General Public License 18 | # along with this program. If not, see . 19 | 20 | from .. import StateMachine 21 | 22 | 23 | class GuiSkeleton: 24 | 25 | def __init__(self, communicator): 26 | 27 | super().__init__() 28 | self._comm = communicator 29 | 30 | def showError(self, state): 31 | 32 | raise NotImplementedError() 33 | 34 | def showWelcome(self, state): 35 | 36 | raise NotImplementedError() 37 | 38 | def showStartup(self, state): 39 | 40 | raise NotImplementedError() 41 | 42 | def showSettings(self, state): 43 | 44 | raise NotImplementedError() 45 | 46 | def showIdle(self, state): 47 | 48 | raise NotImplementedError() 49 | 50 | def showGreeter(self, state): 51 | 52 | raise NotImplementedError() 53 | 54 | def showCountdown(self, state): 55 | 56 | raise NotImplementedError() 57 | 58 | def showCapture(self, state): 59 | 60 | raise NotImplementedError() 61 | 62 | def showAssemble(self, state): 63 | 64 | raise NotImplementedError() 65 | 66 | def showReview(self, state): 67 | 68 | raise NotImplementedError() 69 | 70 | def showPostprocess(self, state): 71 | 72 | raise NotImplementedError() 73 | 74 | def teardown(self, state): 75 | 76 | raise NotImplementedError() 77 | 78 | def handleState(self, state): 79 | 80 | if isinstance(state, StateMachine.CameraEvent): 81 | self.updateCountdown(state) 82 | elif isinstance(state, StateMachine.ErrorState): 83 | self.showError(state) 84 | elif isinstance(state, StateMachine.WelcomeState): 85 | self.showWelcome(state) 86 | elif isinstance(state, StateMachine.StartupState): 87 | self.showStartup(state) 88 | elif isinstance(state, StateMachine.IdleState): 89 | self.showIdle(state) 90 | elif isinstance(state, StateMachine.GreeterState): 91 | self.showGreeter(state) 92 | elif isinstance(state, StateMachine.CountdownState): 93 | self.showCountdown(state) 94 | elif isinstance(state, StateMachine.CaptureState): 95 | self.showCapture(state) 96 | elif isinstance(state, StateMachine.AssembleState): 97 | self.showAssemble(state) 98 | elif isinstance(state, StateMachine.ReviewState): 99 | self.showReview(state) 100 | elif isinstance(state, StateMachine.PostprocessState): 101 | self.showPostprocess(state) 102 | elif isinstance(state, StateMachine.TeardownState): 103 | self.teardown(state) 104 | -------------------------------------------------------------------------------- /photobooth/gui/GuiPostprocessor.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | 4 | # Photobooth - a flexible photo booth software 5 | # Copyright (C) 2018 Balthasar Reuter 6 | # 7 | # This program is free software: you can redistribute it and/or modify 8 | # it under the terms of the GNU Affero General Public License as published by 9 | # the Free Software Foundation, either version 3 of the License, or 10 | # (at your option) any later version. 11 | # 12 | # This program is distributed in the hope that it will be useful, 13 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 14 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 15 | # GNU Affero General Public License for more details. 16 | # 17 | # You should have received a copy of the GNU Affero General Public License 18 | # along with this program. If not, see . 19 | 20 | from .. import printer 21 | from ..util import lookup_and_import 22 | 23 | 24 | class GuiPostprocessor: 25 | 26 | def __init__(self, config): 27 | 28 | super().__init__() 29 | 30 | self._get_task_list = [] 31 | self._do_task_list = [] 32 | 33 | if config.getBool('Printer', 'enable'): 34 | module = config.get('Printer', 'module') 35 | paper_size = (config.getInt('Printer', 'width'), 36 | config.getInt('Printer', 'height')) 37 | pdf = config.getBool('Printer', 'pdf') 38 | if config.getBool('Printer', 'confirmation'): 39 | self._get_task_list.append( 40 | PrintPostprocess(module, paper_size, pdf)) 41 | else: 42 | self._do_task_list.append( 43 | PrintPostprocess(module, paper_size, pdf)) 44 | 45 | def get(self, picture): 46 | 47 | return [task.get(picture) for task in self._get_task_list] 48 | 49 | def do(self, picture): 50 | 51 | for task in self._do_task_list: 52 | task.get(picture).action() 53 | 54 | 55 | class PostprocessTask: 56 | 57 | def __init__(self): 58 | 59 | super().__init__() 60 | 61 | def get(self, picture): 62 | 63 | raise NotImplementedError() 64 | 65 | 66 | class PostprocessItem: 67 | 68 | def __init__(self, label, action): 69 | 70 | super().__init__() 71 | self.label = label 72 | self.action = action 73 | 74 | @property 75 | def label(self): 76 | 77 | return self._label 78 | 79 | @label.setter 80 | def label(self, label): 81 | 82 | if not isinstance(label, str): 83 | raise TypeError('Label must be a string') 84 | 85 | self._label = label 86 | 87 | @property 88 | def action(self): 89 | 90 | return self._action 91 | 92 | @action.setter 93 | def action(self, action): 94 | 95 | if not callable(action): 96 | raise TypeError('Action must be callable') 97 | 98 | self._action = action 99 | 100 | 101 | class PrintPostprocess(PostprocessTask): 102 | 103 | def __init__(self, printer_module, paper_size, is_pdf, **kwargs): 104 | 105 | super().__init__(**kwargs) 106 | 107 | Printer = lookup_and_import(printer.modules, printer_module, 'printer') 108 | self._printer = Printer(paper_size, is_pdf) 109 | 110 | def get(self, picture): 111 | 112 | return PostprocessItem('Print', lambda: self._printer.print(picture)) 113 | -------------------------------------------------------------------------------- /supplementals/state-chart/state-chart.tex: -------------------------------------------------------------------------------- 1 | \documentclass[tikz]{standalone} 2 | \usetikzlibrary{arrows,automata} 3 | \usepackage[utf8]{inputenc} 4 | \begin{document} 5 | \begin{tikzpicture}[->,>=stealth',shorten >=1pt,auto,node distance=5.5cm, 6 | semithick] 7 | \tikzstyle{every state}=[fill=red,draw=none,text=white,minimum width=2cm] 8 | 9 | \node[initial,state] (Welcome) {\texttt{Welcome}}; 10 | \node[state] (Startup) [right of=Welcome] {\texttt{Startup}}; 11 | \node[state] (Teardown) [above of=Welcome] {\texttt{Teardown}}; 12 | \node[state] (Idle) [right of=Startup] {\texttt{Idle}}; 13 | \node[state] (Greeter) [right of=Idle] {\texttt{Greeter}}; 14 | \node[state] (Countdown) [right of=Greeter] {\texttt{Countdown}}; 15 | \node[state] (Capture) [below right of=Countdown]{\texttt{Capture}}; 16 | \node[state] (Assemble) [below left of=Capture] {\texttt{Assemble}}; 17 | \node[state] (Review) [left of=Assemble] {\texttt{Review}}; 18 | \node[state] (Postprocess) [left of=Review] {\texttt{Postprocess}}; 19 | 20 | \node[state] (Error) [above of=Idle] {\texttt{Error}}; 21 | \node[circle,draw,red,text=black] (Any) [left of=Error] {Any state}; 22 | 23 | \path (Welcome) edge node {\texttt{Gui('start')}} (Startup) 24 | (Welcome) edge node [rotate=90,anchor=south] {\texttt{Gui('exit')}} (Teardown) 25 | (Startup) edge node {\texttt{Camera('ready')}} (Idle) 26 | (Idle) edge node {\texttt{Gui('trigger')}} (Greeter) 27 | (Idle) edge [bend left] node {\texttt{Gpio('trigger')}} (Greeter) 28 | (Greeter) edge node {\texttt{Gui('countdown')}} (Countdown) 29 | (Greeter) edge [bend left] node {\texttt{Gpio('countdown')}} (Countdown) 30 | (Countdown) edge [loop above] node {\texttt{Gui('countdown')}} (Countdown) 31 | (Countdown) edge node [rotate=-45,anchor=south] {\texttt{Gui('capture')}} (Capture) 32 | (Capture) edge [bend right] node [rotate=-45,anchor=south] {\texttt{Camera('countdown')}} (Countdown) 33 | (Capture) edge node [rotate=45,anchor=north] {\texttt{Camera('assemble')}} (Assemble) 34 | (Assemble) edge node {\texttt{Camera('review')}} (Review) 35 | (Review) edge node {\texttt{Gui('postprocess')}} (Postprocess) 36 | (Postprocess) edge node [rotate=90,anchor=south] {\texttt{Gui('idle')}} (Idle) 37 | (Postprocess) edge [bend left] node [rotate=90,anchor=south] {\texttt{Gpio('idle')}} (Idle) 38 | (Any) edge node {\texttt{Error(msg)}} (Error) 39 | (Error) edge node [rotate=-90,anchor=south] {\texttt{Gui('abort')}} (Idle) 40 | (Error) edge [bend left] node {\texttt{Gui('retry')}} (Any) 41 | (Any) edge node [rotate=45,anchor=north] {\texttt{Teardown(WELCOME)}} (Welcome); 42 | 43 | \node [text width=10cm,anchor=north west] at (current page.south west) 44 | { 45 | States and transitions are implemented in \texttt{StateMachine.py}. \\[1.2em] 46 | All state names are presented without the name part \texttt{...State}, i.\,e., they are actually called \texttt{WelcomeState}, \texttt{IdleState}, etc. \\[1.2em] 47 | The same applies to event names, they are actually called \texttt{GuiEvent}, \texttt{CameraEvent}, etc. 48 | }; 49 | \end{tikzpicture} 50 | \end{document} -------------------------------------------------------------------------------- /photobooth/gui/Qt5Gui/images/checkmark.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 19 | 21 | 25 | 32 | 39 | 40 | 44 | 51 | 58 | 59 | 60 | 82 | 84 | 85 | 87 | image/svg+xml 88 | 90 | 91 | 92 | 93 | 94 | 99 | 104 | 109 | 110 | 111 | -------------------------------------------------------------------------------- /photobooth/worker/PictureMailer.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | 4 | # Photobooth - a flexible photo booth software 5 | # Copyright (C) 2018 Balthasar Reuter 6 | # 7 | # This program is free software: you can redistribute it and/or modify 8 | # it under the terms of the GNU Affero General Public License as published by 9 | # the Free Software Foundation, either version 3 of the License, or 10 | # (at your option) any later version. 11 | # 12 | # This program is distributed in the hope that it will be useful, 13 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 14 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 15 | # GNU Affero General Public License for more details. 16 | # 17 | # You should have received a copy of the GNU Affero General Public License 18 | # along with this program. If not, see . 19 | 20 | import logging 21 | import smtplib 22 | 23 | from email.mime.multipart import MIMEMultipart 24 | from email.mime.base import MIMEBase 25 | from email.mime.text import MIMEText 26 | from email.utils import formatdate 27 | from email import encoders 28 | 29 | from pathlib import Path 30 | 31 | from .WorkerTask import WorkerTask 32 | 33 | 34 | def send_mail(send_from, send_to, subject, message, picture, filename, 35 | server, port, is_auth, username, password, is_tls): 36 | """Compose and send email with provided info and attachments. 37 | 38 | Based on https://stackoverflow.com/a/16509278 39 | 40 | Args: 41 | send_from (str): from name 42 | send_to (str): to name 43 | subject (str): message title 44 | message (str): message body 45 | picture (jpg byte_data): ByteIO data of the JPG picture 46 | filename (str): Filename of picture 47 | server (str): mail server host name 48 | port (int): port number 49 | is_auth (bool): server requires authentication 50 | username (str): server auth username 51 | password (str): server auth password 52 | is_tls (bool): use TLS mode 53 | """ 54 | msg = MIMEMultipart() 55 | msg['From'] = send_from 56 | msg['To'] = send_to 57 | msg['Date'] = formatdate(localtime=True) 58 | msg['Subject'] = subject 59 | 60 | msg.attach(MIMEText(message)) 61 | 62 | part = MIMEBase('application', "octet-stream") 63 | part.set_payload(picture.getbuffer()) 64 | encoders.encode_base64(part) 65 | part.add_header('Content-Disposition', 66 | 'attachment; filename="{}"'.format(filename)) 67 | msg.attach(part) 68 | 69 | smtp = smtplib.SMTP(server, port) 70 | if is_tls: 71 | smtp.starttls() 72 | if is_auth: 73 | smtp.login(username, password) 74 | smtp.sendmail(send_from, send_to, msg.as_string()) 75 | smtp.quit() 76 | 77 | 78 | class PictureMailer(WorkerTask): 79 | 80 | def __init__(self, config): 81 | 82 | super().__init__() 83 | 84 | self._sender = config.get('Mailer', 'sender') 85 | self._recipient = config.get('Mailer', 'recipient') 86 | self._subject = config.get('Mailer', 'subject') 87 | self._message = config.get('Mailer', 'message') 88 | 89 | self._server = config.get('Mailer', 'server') 90 | self._port = config.getInt('Mailer', 'port') 91 | self._is_auth = config.getBool('Mailer', 'use_auth') 92 | self._user = config.get('Mailer', 'user') 93 | self._password = config.get('Mailer', 'password') 94 | self._is_tls = config.getBool('Mailer', 'use_tls') 95 | 96 | def do(self, picture, filename): 97 | 98 | logging.info('Sending picture to %s', self._recipient) 99 | send_mail(self._sender, self._recipient, self._subject, self._message, 100 | picture, Path(filename).name, self._server, self._port, 101 | self._is_auth, self._user, self._password, self._is_tls) 102 | -------------------------------------------------------------------------------- /photobooth/worker/__init__.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | 4 | # Photobooth - a flexible photo booth software 5 | # Copyright (C) 2018 Balthasar Reuter 6 | # 7 | # This program is free software: you can redistribute it and/or modify 8 | # it under the terms of the GNU Affero General Public License as published by 9 | # the Free Software Foundation, either version 3 of the License, or 10 | # (at your option) any later version. 11 | # 12 | # This program is distributed in the hope that it will be useful, 13 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 14 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 15 | # GNU Affero General Public License for more details. 16 | # 17 | # You should have received a copy of the GNU Affero General Public License 18 | # along with this program. If not, see . 19 | 20 | import os.path 21 | 22 | from time import localtime, strftime 23 | 24 | from .. import StateMachine 25 | from ..Threading import Workers 26 | 27 | from .PictureList import PictureList 28 | from .PictureMailer import PictureMailer 29 | from .PictureSaver import PictureSaver 30 | from .PictureUploadWebdav import PictureUploadWebdav 31 | 32 | 33 | class Worker: 34 | 35 | def __init__(self, config, comm): 36 | 37 | self._comm = comm 38 | 39 | # Picture list for assembled pictures 40 | path = os.path.join(config.get('Storage', 'basedir'), 41 | config.get('Storage', 'basename')) 42 | basename = strftime(path, localtime()) 43 | self._pic_list = PictureList(basename) 44 | 45 | # Picture list for individual shots 46 | path = os.path.join(config.get('Storage', 'basedir'), 47 | config.get('Storage', 'basename') + '_shot_') 48 | basename = strftime(path, localtime()) 49 | self._shot_list = PictureList(basename) 50 | 51 | self.initPostprocessTasks(config) 52 | self.initPictureTasks(config) 53 | 54 | def initPostprocessTasks(self, config): 55 | 56 | self._postprocess_tasks = [] 57 | 58 | # PictureSaver for assembled pictures 59 | self._postprocess_tasks.append(PictureSaver(self._pic_list.basename)) 60 | 61 | # PictureMailer for assembled pictures 62 | if config.getBool('Mailer', 'enable'): 63 | self._postprocess_tasks.append(PictureMailer(config)) 64 | 65 | # PictureUploadWebdav to upload pictures to a webdav storage 66 | if config.getBool('UploadWebdav', 'enable'): 67 | self._postprocess_tasks.append(PictureUploadWebdav(config)) 68 | 69 | def initPictureTasks(self, config): 70 | 71 | self._picture_tasks = [] 72 | 73 | # PictureSaver for single shots 74 | self._picture_tasks.append(PictureSaver(self._shot_list.basename)) 75 | 76 | def run(self): 77 | 78 | for state in self._comm.iter(Workers.WORKER): 79 | self.handleState(state) 80 | 81 | return True 82 | 83 | def handleState(self, state): 84 | 85 | if isinstance(state, StateMachine.TeardownState): 86 | self.teardown(state) 87 | elif isinstance(state, StateMachine.ReviewState): 88 | self.doPostprocessTasks(state.picture, self._pic_list.getNext()) 89 | elif isinstance(state, StateMachine.CameraEvent): 90 | if state.name == 'capture': 91 | self.doPictureTasks(state.picture, self._shot_list.getNext()) 92 | else: 93 | raise ValueError('Unknown CameraEvent "{}"'.format(state)) 94 | 95 | def teardown(self, state): 96 | 97 | pass 98 | 99 | def doPostprocessTasks(self, picture, filename): 100 | 101 | for task in self._postprocess_tasks: 102 | task.do(picture, filename) 103 | 104 | def doPictureTasks(self, picture, filename): 105 | 106 | for task in self._picture_tasks: 107 | task.do(picture, filename) 108 | -------------------------------------------------------------------------------- /photobooth/defaults.cfg: -------------------------------------------------------------------------------- 1 | [Gui] 2 | # Gui module to use (PyQt5) 3 | module = PyQt5 4 | # Start Photobooth in fullscreen mode (True/False) 5 | fullscreen = False 6 | # Width of Photobooth (if not fullscreen) 7 | width = 1024 8 | # Height of Photobooth (if not fullscreen) 9 | height = 600 10 | # Hide cursor 11 | hide_cursor = False 12 | # Use specified style 13 | style = default 14 | 15 | [Camera] 16 | # Camera module to use (python-gphoto2, gphoto2-cffi, gphoto2-commandline, 17 | # opencv, picamera, dummy) 18 | module = python-gphoto2 19 | # Specify rotation of camera in degree (possible values: 0, 90, 180, 270) 20 | rotation = 0 21 | 22 | [Gpio] 23 | # Enable use of GPIO (True/False) 24 | enable = False 25 | # BOARD pin 18 (BCM pin 24) lets you return to start screen 26 | exit_pin = 24 27 | # BOARD pin 16 (BCM pin 23) triggers capturing pictures 28 | trigger_pin = 23 29 | # BOARD pin 7 (BCM pin 4) switches the lamp on and off 30 | lamp_pin = 4 31 | # BOARD pin (BCM pin 17) switches the blue channel 32 | chan_b_pin = 17 33 | # BOARD pin (BCM pin 27) switches the red channel 34 | chan_r_pin = 27 35 | # BOARD pin (BCM pin 22) switches the green channel 36 | chan_g_pin = 22 37 | 38 | [Printer] 39 | # Enable printing (True/False) 40 | enable = True 41 | # Print to PDF (True/False) for debugging purposes 42 | pdf = False 43 | # Ask for confirmation before printing 44 | confirmation = True 45 | # Printer module to use (PyQt5, PyCUPS) 46 | module = PyQt5 47 | # Paper width in mm 48 | width = 148 49 | # Paper height in mm 50 | height = 100 51 | 52 | [Photobooth] 53 | # Show preview while posing time (True/False) 54 | show_preview = True 55 | # Greeter time in seconds (shown before countdown) 56 | greeter_time = 3 57 | # Countdown length in seconds (shown before every shot) 58 | countdown_time = 8 59 | # Display time of assembled picture (shown after last shot) 60 | display_time = 5 61 | # Timeout for postprocessing (shown after review) 62 | postprocess_time = 60 63 | # Overwrite displayed error message (Leave empty for none) 64 | overwrite_error_message = 65 | 66 | [Picture] 67 | # Number of pictures in horizontal direction 68 | num_x = 2 69 | # Number of pictures in vertical direction 70 | num_y = 2 71 | # Size of output picture in horizontal direction 72 | size_x = 3496 73 | # Size of output picture in vertical direction 74 | size_y = 2362 75 | # Minimum distance between thumbnails in horizontal direction 76 | inner_dist_x = 20 77 | # Minimum distance between thumbnails in vertical direction 78 | inner_dist_y = 20 79 | # Minimum distance of thumbnails to border in horizontal direction 80 | outer_dist_x = 40 81 | # Minimum distance of thumbnails to border in vertical direction 82 | outer_dist_y = 40 83 | # Leave out the specified pictures, e.g. for a logo (comma-separated list) 84 | skip = 85 | # Specify background image (filename, optional) 86 | background = 87 | 88 | [Storage] 89 | # Basedir of output pictures 90 | basedir = %Y-%m-%d 91 | # Basename of output pictures 92 | basename = photobooth 93 | # Keep single pictures (True/False) 94 | keep_pictures = False 95 | 96 | [Mailer] 97 | # Enable/disable mailer 98 | enable = False 99 | # Sender address 100 | sender = photobooth@example.com 101 | # Recipient address 102 | recipient = photobooth@example.com 103 | # Mail subject 104 | subject = A new picture from the photobooth 105 | # Mail message 106 | message = Sent by the photobooth (https://github.com/reuterbal/photobooth) 107 | # SMTP server name 108 | server = localhost 109 | # SMTP server port 110 | port = 25 111 | # SMTP server requires authentication 112 | use_auth = True 113 | # SMTP username 114 | user = 115 | # SMTP password 116 | password = 117 | # SSL connection 118 | use_tls = False 119 | 120 | [UploadWebdav] 121 | # Enable/disable webdav upload 122 | enable = False 123 | # URL at webdav server where files should be uploaded 124 | url = https://example.com/remote.php/webdav/Photobooth/ 125 | # Webdav server requires authentication 126 | use_auth = True 127 | # Webdav username 128 | user = 129 | # Webdav password 130 | password = 131 | -------------------------------------------------------------------------------- /photobooth/gui/Qt5Gui/images/camera.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 19 | 21 | 22 | 24 | image/svg+xml 25 | 27 | 28 | 29 | 30 | 31 | 33 | 37 | 44 | 51 | 52 | 53 | 77 | 82 | 83 | -------------------------------------------------------------------------------- /photobooth/gui/Qt5Gui/fonts/OFL.txt: -------------------------------------------------------------------------------- 1 | Copyright 2015 The Amatic SC Project Authors (https://github.com/googlefonts/AmaticSC) 2 | 3 | This Font Software is licensed under the SIL Open Font License, Version 1.1. 4 | This license is copied below, and is also available with a FAQ at: 5 | http://scripts.sil.org/OFL 6 | 7 | 8 | ----------------------------------------------------------- 9 | SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 10 | ----------------------------------------------------------- 11 | 12 | PREAMBLE 13 | The goals of the Open Font License (OFL) are to stimulate worldwide 14 | development of collaborative font projects, to support the font creation 15 | efforts of academic and linguistic communities, and to provide a free and 16 | open framework in which fonts may be shared and improved in partnership 17 | with others. 18 | 19 | The OFL allows the licensed fonts to be used, studied, modified and 20 | redistributed freely as long as they are not sold by themselves. The 21 | fonts, including any derivative works, can be bundled, embedded, 22 | redistributed and/or sold with any software provided that any reserved 23 | names are not used by derivative works. The fonts and derivatives, 24 | however, cannot be released under any other type of license. The 25 | requirement for fonts to remain under this license does not apply 26 | to any document created using the fonts or their derivatives. 27 | 28 | DEFINITIONS 29 | "Font Software" refers to the set of files released by the Copyright 30 | Holder(s) under this license and clearly marked as such. This may 31 | include source files, build scripts and documentation. 32 | 33 | "Reserved Font Name" refers to any names specified as such after the 34 | copyright statement(s). 35 | 36 | "Original Version" refers to the collection of Font Software components as 37 | distributed by the Copyright Holder(s). 38 | 39 | "Modified Version" refers to any derivative made by adding to, deleting, 40 | or substituting -- in part or in whole -- any of the components of the 41 | Original Version, by changing formats or by porting the Font Software to a 42 | new environment. 43 | 44 | "Author" refers to any designer, engineer, programmer, technical 45 | writer or other person who contributed to the Font Software. 46 | 47 | PERMISSION & CONDITIONS 48 | Permission is hereby granted, free of charge, to any person obtaining 49 | a copy of the Font Software, to use, study, copy, merge, embed, modify, 50 | redistribute, and sell modified and unmodified copies of the Font 51 | Software, subject to the following conditions: 52 | 53 | 1) Neither the Font Software nor any of its individual components, 54 | in Original or Modified Versions, may be sold by itself. 55 | 56 | 2) Original or Modified Versions of the Font Software may be bundled, 57 | redistributed and/or sold with any software, provided that each copy 58 | contains the above copyright notice and this license. These can be 59 | included either as stand-alone text files, human-readable headers or 60 | in the appropriate machine-readable metadata fields within text or 61 | binary files as long as those fields can be easily viewed by the user. 62 | 63 | 3) No Modified Version of the Font Software may use the Reserved Font 64 | Name(s) unless explicit written permission is granted by the corresponding 65 | Copyright Holder. This restriction only applies to the primary font name as 66 | presented to the users. 67 | 68 | 4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font 69 | Software shall not be used to promote, endorse or advertise any 70 | Modified Version, except to acknowledge the contribution(s) of the 71 | Copyright Holder(s) and the Author(s) or with their explicit written 72 | permission. 73 | 74 | 5) The Font Software, modified or unmodified, in part or in whole, 75 | must be distributed entirely under this license, and must not be 76 | distributed under any other license. The requirement for fonts to 77 | remain under this license does not apply to any document created 78 | using the Font Software. 79 | 80 | TERMINATION 81 | This license becomes null and void if any of the above conditions are 82 | not met. 83 | 84 | DISCLAIMER 85 | THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 86 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF 87 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT 88 | OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE 89 | COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 90 | INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL 91 | DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 92 | FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM 93 | OTHER DEALINGS IN THE FONT SOFTWARE. 94 | -------------------------------------------------------------------------------- /photobooth/camera/CameraGphoto2Cffi.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | 4 | # Photobooth - a flexible photo booth software 5 | # Copyright (C) 2018 Balthasar Reuter 6 | # 7 | # This program is free software: you can redistribute it and/or modify 8 | # it under the terms of the GNU Affero General Public License as published by 9 | # the Free Software Foundation, either version 3 of the License, or 10 | # (at your option) any later version. 11 | # 12 | # This program is distributed in the hope that it will be useful, 13 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 14 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 15 | # GNU Affero General Public License for more details. 16 | # 17 | # You should have received a copy of the GNU Affero General Public License 18 | # along with this program. If not, see . 19 | 20 | import io 21 | import logging 22 | 23 | from PIL import Image 24 | 25 | import gphoto2cffi as gp 26 | 27 | from .CameraInterface import CameraInterface 28 | 29 | 30 | class CameraGphoto2Cffi(CameraInterface): 31 | 32 | def __init__(self): 33 | 34 | super().__init__() 35 | 36 | self.hasPreview = True 37 | self.hasIdle = True 38 | 39 | logging.info('Using gphoto2-cffi bindings') 40 | 41 | self._setupCamera() 42 | 43 | def cleanup(self): 44 | 45 | try: 46 | self._cap.config['imgsettings']['imageformat'].set(self._imgfmt) 47 | self._cap.config['imgsettings']['imageformatsd'].set( 48 | self._imgfmtsd) 49 | # self._cap.config['settings']['autopoweroff'].set( 50 | # self._autopoweroff) 51 | except BaseException as e: 52 | logging.warn('Error while changing camera settings: {}.'.format(e)) 53 | 54 | def _setupCamera(self): 55 | 56 | self._cap = gp.Camera() 57 | logging.info('Supported operations: %s', 58 | self._cap.supported_operations) 59 | 60 | try: 61 | # make sure camera format is not set to raw 62 | imgfmt = 'Large Fine JPEG' 63 | self._imgfmt = self._cap.config['imgsettings']['imageformat'].value 64 | if 'raw' in self._imgfmt.lower(): 65 | self._cap.config['imgsettings']['imageformat'].set(imgfmt) 66 | self._imgfmtsd = ( 67 | self._cap.config['imgsettings']['imageformatsd'].value) 68 | if 'raw' in self._imgfmtsd.lower(): 69 | self._cap.config['imgsettings']['imageformatsd'].set(imgfmt) 70 | 71 | # make sure autopoweroff is disabled 72 | # this doesn't seem to work 73 | # self._autopoweroff = int( 74 | # self._cap.config['settings']['autopoweroff'].value) 75 | # if self._autopoweroff > 0: 76 | # self._cap.config['settings']['autopoweroff'].set("0") 77 | except BaseException as e: 78 | logging.warn('Error while changing camera settings: {}.'.format(e)) 79 | 80 | # print current config 81 | self._printConfig(self._cap.config) 82 | 83 | @staticmethod 84 | def _configTreeToText(config, indent=0): 85 | 86 | config_txt = '' 87 | 88 | for k, v in config.items(): 89 | config_txt += indent * ' ' 90 | config_txt += k + ': ' 91 | 92 | if hasattr(v, '__len__') and len(v) > 1: 93 | config_txt += '\n' 94 | config_txt += CameraGphoto2Cffi._configTreeToText(v, 95 | indent + 4) 96 | else: 97 | config_txt += str(v) + '\n' 98 | 99 | return config_txt 100 | 101 | @staticmethod 102 | def _printConfig(config): 103 | config_txt = 'Camera configuration:\n' 104 | config_txt += CameraGphoto2Cffi._configTreeToText(config) 105 | logging.info(config_txt) 106 | 107 | def setActive(self): 108 | 109 | try: 110 | self._cap._get_config()['actions']['viewfinder'].set(True) 111 | self._cap._get_config()['settings']['output'].set('PC') 112 | except BaseException as e: 113 | logging.warn('Cannot set camera output to active: {}.'.format(e)) 114 | 115 | def setIdle(self): 116 | 117 | try: 118 | self._cap._get_config()['actions']['viewfinder'].set(False) 119 | self._cap._get_config()['settings']['output'].set('Off') 120 | except BaseException as e: 121 | logging.warn('Cannot set camera output to idle: {}.'.format(e)) 122 | 123 | def getPreview(self): 124 | 125 | return Image.open(io.BytesIO(self._cap.get_preview())) 126 | 127 | def getPicture(self): 128 | 129 | return Image.open(io.BytesIO(self._cap.capture())) 130 | -------------------------------------------------------------------------------- /photobooth/camera/CameraGphoto2.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | 4 | # Photobooth - a flexible photo booth software 5 | # Copyright (C) 2018 Balthasar Reuter 6 | # 7 | # This program is free software: you can redistribute it and/or modify 8 | # it under the terms of the GNU Affero General Public License as published by 9 | # the Free Software Foundation, either version 3 of the License, or 10 | # (at your option) any later version. 11 | # 12 | # This program is distributed in the hope that it will be useful, 13 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 14 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 15 | # GNU Affero General Public License for more details. 16 | # 17 | # You should have received a copy of the GNU Affero General Public License 18 | # along with this program. If not, see . 19 | 20 | import io 21 | import logging 22 | 23 | from PIL import Image 24 | 25 | import gphoto2 as gp 26 | 27 | from .CameraInterface import CameraInterface 28 | 29 | 30 | class CameraGphoto2(CameraInterface): 31 | 32 | def __init__(self): 33 | 34 | super().__init__() 35 | 36 | self.hasPreview = True 37 | self.hasIdle = True 38 | 39 | logging.info('Using python-gphoto2 bindings') 40 | 41 | self._setupLogging() 42 | self._setupCamera() 43 | 44 | def cleanup(self): 45 | 46 | self._changeConfig('Shutdown') 47 | self._cap.exit(self._ctxt) 48 | 49 | def _setupLogging(self): 50 | 51 | gp.error_severity[gp.GP_ERROR] = logging.ERROR 52 | gp.check_result(gp.use_python_logging()) 53 | 54 | def _setupCamera(self): 55 | 56 | self._ctxt = gp.Context() 57 | self._cap = gp.Camera() 58 | self._cap.init(self._ctxt) 59 | 60 | logging.info('Camera summary: %s', 61 | str(self._cap.get_summary(self._ctxt))) 62 | 63 | # read model specific configuration 64 | config = self._cap.get_config() 65 | self.loadConfig(config.get_child_by_name('cameramodel').get_value()) 66 | 67 | # set startup configuration 68 | self._changeConfig('Startup') 69 | 70 | # print current config 71 | self._printConfig(self._cap.get_config()) 72 | 73 | @staticmethod 74 | def _configTreeToText(tree, indent=0): 75 | 76 | config_txt = '' 77 | 78 | for chld in tree.get_children(): 79 | config_txt += indent * ' ' 80 | config_txt += chld.get_label() + ' [' + chld.get_name() + ']: ' 81 | 82 | if chld.count_children() > 0: 83 | config_txt += '\n' 84 | config_txt += CameraGphoto2._configTreeToText(chld, indent + 4) 85 | else: 86 | config_txt += str(chld.get_value()) 87 | try: 88 | choice_txt = ' (' 89 | 90 | for c in chld.get_choices(): 91 | choice_txt += c + ', ' 92 | 93 | choice_txt += ')' 94 | config_txt += choice_txt 95 | except gp.GPhoto2Error: 96 | pass 97 | config_txt += '\n' 98 | 99 | return config_txt 100 | 101 | @staticmethod 102 | def _printConfig(config): 103 | 104 | config_txt = 'Camera configuration:\n' 105 | config_txt += CameraGphoto2._configTreeToText(config) 106 | logging.info(config_txt) 107 | 108 | def _changeConfig(self, state): 109 | 110 | if self.config[state]: 111 | config = self._cap.get_config() 112 | 113 | for key in self.config[state]: 114 | val = config.get_child_by_name(key) 115 | if val.get_value().lower() != self.config[state][key].lower(): 116 | val.set_value(self.config[state][key]) 117 | 118 | try: 119 | self._cap.set_config(config) 120 | except BaseException as e: 121 | logging.warn(('CameraGphoto2: Applying config for state ' 122 | '"{}" failed: {}').format(state, e)) 123 | 124 | def setActive(self): 125 | 126 | self._changeConfig('Active') 127 | 128 | def setIdle(self): 129 | 130 | self._changeConfig('Idle') 131 | 132 | def getPreview(self): 133 | 134 | camera_file = self._cap.capture_preview() 135 | file_data = camera_file.get_data_and_size() 136 | return Image.open(io.BytesIO(file_data)) 137 | 138 | def getPicture(self): 139 | 140 | file_path = self._cap.capture(gp.GP_CAPTURE_IMAGE) 141 | camera_file = self._cap.file_get(file_path.folder, file_path.name, 142 | gp.GP_FILE_TYPE_NORMAL) 143 | file_data = camera_file.get_data_and_size() 144 | return Image.open(io.BytesIO(file_data)) 145 | -------------------------------------------------------------------------------- /photobooth/gui/Qt5Gui/images/arrow.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 19 | 21 | 25 | 32 | 39 | 40 | 44 | 51 | 58 | 59 | 63 | 70 | 77 | 78 | 79 | 101 | 103 | 104 | 106 | image/svg+xml 107 | 109 | 110 | 111 | 112 | 113 | 118 | 123 | 128 | 133 | 134 | 135 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # photobooth 2 | 3 | [![Buy me a coffee](https://www.buymeacoffee.com/assets/img/custom_images/black_img.png)](https://www.buymeacoffee.com/reuterbal) 4 | 5 | A flexible Photobooth software. 6 | 7 | It supports many different camera models, the appearance can be adapted to your likings, and it runs on many different hardware setups. 8 | 9 | ## Description 10 | This is a Python application to build your own photobooth. 11 | 12 | ### Features 13 | * Capture a single or multiple pictures and assemble them in an m-by-n grid layout 14 | * Live preview during countdown 15 | * Store assembled pictures (and optionally the individual shots) 16 | * Printing of captured pictures (via Qt printing module or pycups) 17 | * Highly customizable via settings menu inside the graphical user interface 18 | * Custom background for assembled pictures 19 | * Ability to skip single pictures in the m-by-n grid (e.g., to include a logo in the background image) 20 | * Support for external buttons and lamps via GPIO interface 21 | * Rudimentary WebDAV upload functionality (saves pictures to WebDAV storage) and mailer feature (mails pictures to a fixed email address) 22 | * Theming support using [Qt stylesheets](https://doc.qt.io/qt-5/stylesheet-syntax.html) 23 | 24 | ### Screenshots 25 | Screenshots produced using `CameraDummy` that produces unicolor images. 26 | 27 | #### Theme _pastel_ 28 | Idle screen Greeter screen Countdown screen Postprocessing screen Settings screen 29 | 30 | #### Theme _dark_ 31 | Idle screen Greeter screen Countdown screen Postprocessing screen 32 | 33 | ### Technical specifications 34 | * Many camera models supported, thanks to interfaces to [gPhoto2](http://www.gphoto.org/), [OpenCV](https://opencv.org/), [Raspberry Pi camera module](https://projects.raspberrypi.org/en/projects/getting-started-with-picamera) 35 | * Tested on Standard x86 hardware and [Raspberry Pi](https://raspberrypi.org/) models 1B+, 2B, 3B, and 3B+ 36 | * Flexible, modular design: Easy to add features or customize the appearance 37 | * Multi-threaded for responsive GUI and fast processing 38 | * Based on [Python 3](https://www.python.org/), [Pillow](https://pillow.readthedocs.io), and [Qt5](https://www.qt.io/developers/) 39 | 40 | ### History 41 | I started this project for my own wedding in 2015. 42 | See [Version 0.1](https://github.com/reuterbal/photobooth/tree/v0.1) for the original version. 43 | Github user [hackerb9](https://github.com/hackerb9/photobooth) forked this version and added a print functionality. 44 | However, I was not happy with the original software design and the limited options provided by the previously used [pygame](https://www.pygame.org) GUI library and thus abandoned the original version. 45 | Since then it underwent a complete rewrite, with vastly improved performance and a much more modular and mature software design. 46 | 47 | ## Installation and usage 48 | 49 | ### Hardware requirements 50 | * Some computer/SoC that is able to run Python 3.5+ as well as any of the supported camera libraries 51 | * Camera supported by gPhoto 2 (see [compatibility list](http://gphoto.org/proj/libgphoto2/support.php)), OpenCV (e.g., most standard webcams), or a Raspberry Pi Camera Module. 52 | * Optional: External buttons and lamps (in combination with gpiozero-compatible hardware) 53 | 54 | ### Installing and running the photobooth 55 | 56 | See [installation instructions](INSTALL.md). 57 | 58 | ## Configuration and modifications 59 | Default settings are stored in [`defaults.cfg`](photobooth/defaults.cfg) and can either be changed in the graphical user interface or by creating a file `photobooth.cfg` in the top folder and overwriting your settings there. 60 | 61 | The software design is very modular. 62 | Feel free to add new postprocessing components, a GUI based on some other library, etc. 63 | 64 | ## Feedback and bugs 65 | I appreciate any feedback or bug reports. 66 | Please submit them via the [Issue tracker](https://github.com/reuterbal/photobooth/issues/new?template=bug_report.md) and always include your `photobooth.log` file (is created automatically in the top folder) and a description of your hardware and software setup. 67 | 68 | I am also happy to hear any success stories! Feel free to [submit them here](https://github.com/reuterbal/photobooth/issues/new?template=success-story.md). 69 | 70 | If you find this application useful, please consider [buying me a coffee](https://www.buymeacoffee.com/reuterbal). 71 | 72 | 73 | ## License 74 | I provide this code under AGPL v3. See [LICENSE](https://github.com/reuterbal/photobooth/blob/master/LICENSE.txt). 75 | -------------------------------------------------------------------------------- /supplementals/housing/Cuttertemplate.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 20 | 22 | 25 | 29 | 30 | 31 | 49 | 51 | 52 | 54 | image/svg+xml 55 | 57 | 58 | 59 | 60 | 61 | 66 | 71 | 78 | 85 | 93 | 101 | 109 | 117 | 121 | 126 | 131 | 132 | 133 | -------------------------------------------------------------------------------- /photobooth/camera/PictureDimensions.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | 4 | # Photobooth - a flexible photo booth software 5 | # Copyright (C) 2018 Balthasar Reuter 6 | # 7 | # This program is free software: you can redistribute it and/or modify 8 | # it under the terms of the GNU Affero General Public License as published by 9 | # the Free Software Foundation, either version 3 of the License, or 10 | # (at your option) any later version. 11 | # 12 | # This program is distributed in the hope that it will be useful, 13 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 14 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 15 | # GNU Affero General Public License for more details. 16 | # 17 | # You should have received a copy of the GNU Affero General Public License 18 | # along with this program. If not, see . 19 | 20 | import logging 21 | 22 | 23 | class PictureDimensions: 24 | 25 | def __init__(self, config, capture_size): 26 | 27 | self._num_pictures = (config.getInt('Picture', 'num_x'), 28 | config.getInt('Picture', 'num_y')) 29 | 30 | self._capture_size = capture_size 31 | 32 | self._output_size = (config.getInt('Picture', 'size_x'), 33 | config.getInt('Picture', 'size_y')) 34 | 35 | self._inner_distance = (config.getInt('Picture', 'inner_dist_x'), 36 | config.getInt('Picture', 'inner_dist_y')) 37 | self._outer_distance = (config.getInt('Picture', 'outer_dist_x'), 38 | config.getInt('Picture', 'outer_dist_y')) 39 | 40 | self._skip = [i for i in config.getIntList('Picture', 'skip') 41 | if 1 <= i and 42 | i <= self._num_pictures[0] * self._num_pictures[1]] 43 | 44 | self.computeThumbnailDimensions() 45 | 46 | self.computePreviewDimensions(config) 47 | 48 | def _computeResizeFactor(self, coord, inner_size): 49 | 50 | return ((inner_size - (self.numPictures[coord] + 1) * 51 | self.innerDistance[coord]) / 52 | (self.numPictures[coord] * self.captureSize[coord])) 53 | 54 | def _computeThumbOffset(self, coord, inner_size): 55 | 56 | return (inner_size - self.numPictures[coord] * 57 | self.thumbnailSize[coord]) // (self.numPictures[coord] + 1) 58 | 59 | def computeThumbnailDimensions(self): 60 | 61 | border = tuple(self.outerDistance[i] - self.innerDistance[i] 62 | for i in range(2)) 63 | inner_size = tuple(self.outputSize[i] - 2 * border[i] 64 | for i in range(2)) 65 | 66 | resize_factor = min(self._computeResizeFactor(i, inner_size[i]) 67 | for i in range(2)) 68 | self._thumb_size = tuple(int(self.captureSize[i] * resize_factor) 69 | for i in range(2)) 70 | 71 | thumb_dist = tuple(self._computeThumbOffset(i, inner_size[i]) 72 | for i in range(2)) 73 | 74 | thumbs = [i for i in range(self.numPictures[0] * self.numPictures[1]) 75 | if i + 1 not in self._skip] 76 | 77 | self._thumb_offsets = [] 78 | for i in thumbs: 79 | pos = (i % self.numPictures[0], i // self.numPictures[0]) 80 | self._thumb_offsets.append(tuple(border[j] + 81 | (pos[j] + 1) * thumb_dist[j] + 82 | pos[j] * self.thumbnailSize[j] 83 | for j in range(2))) 84 | 85 | logging.debug(('Assembled picture will contain {} ({}x{}) pictures ' 86 | 'in positions {}').format(self.totalNumPictures, 87 | self.numPictures[0], 88 | self.numPictures[1], thumbs)) 89 | 90 | def computePreviewDimensions(self, config): 91 | 92 | gui_size = (config.getInt('Gui', 'width'), 93 | config.getInt('Gui', 'height')) 94 | 95 | resize_factor = min(min((gui_size[i] / self.captureSize[i] 96 | for i in range(2))), 1) 97 | 98 | self._preview_size = tuple(int(self.captureSize[i] * resize_factor) 99 | for i in range(2)) 100 | 101 | @property 102 | def numPictures(self): 103 | 104 | return self._num_pictures 105 | 106 | @property 107 | def totalNumPictures(self): 108 | 109 | return max(self._num_pictures[0] * self._num_pictures[1] - 110 | len(self._skip), 1) 111 | 112 | @property 113 | def skipLast(self): 114 | 115 | return self._skip_last 116 | 117 | @property 118 | def captureSize(self): 119 | 120 | return self._capture_size 121 | 122 | @property 123 | def outputSize(self): 124 | 125 | return self._output_size 126 | 127 | @property 128 | def innerDistance(self): 129 | 130 | return self._inner_distance 131 | 132 | @property 133 | def outerDistance(self): 134 | 135 | return self._outer_distance 136 | 137 | @property 138 | def thumbnailSize(self): 139 | 140 | return self._thumb_size 141 | 142 | @property 143 | def thumbnailOffset(self): 144 | 145 | return self._thumb_offsets 146 | 147 | @property 148 | def previewSize(self): 149 | 150 | return self._preview_size 151 | -------------------------------------------------------------------------------- /photobooth/gui/Qt5Gui/stylesheets/dark-800x600.qss: -------------------------------------------------------------------------------- 1 | /* Outer items */ 2 | 3 | QWidget { 4 | background-color: transparent; 5 | color: #eeeeee; 6 | font-family: AmaticSC, sans-serif; 7 | font-size: 30px; 8 | } 9 | 10 | QMainWindow { 11 | background: #000000; 12 | color: #eeeeee; 13 | } 14 | 15 | /* General controls */ 16 | 17 | QPushButton { 18 | background-color: transparent; 19 | border-style: outset; 20 | border-width: 1px; 21 | border-radius: 8px; 22 | border-color: #eeeeee; 23 | padding: 8px; 24 | } 25 | 26 | QPushButton:pressed { 27 | background-color: #66eeeeee; 28 | } 29 | 30 | /* Idle Screen */ 31 | 32 | QFrame#IdleMessage { 33 | background-image: url(photobooth/gui/Qt5Gui/images/arrow-800x600.png); 34 | background-repeat: no-repeat; 35 | padding: 80px 40px 60px 40px; 36 | } 37 | 38 | QFrame#IdleMessage QLabel { 39 | font-size: 80px; 40 | qproperty-alignment: AlignCenter; 41 | } 42 | 43 | QFrame#IdleMessage QPushButton { 44 | color: rgba(255, 27, 0, 200); 45 | font-size: 120px; 46 | text-align: center; 47 | } 48 | 49 | QFrame#IdleMessage QPushButton:pressed { 50 | background-color: rgba(255, 27, 0, 200); 51 | color: #eeeeee; 52 | } 53 | 54 | /* Greeter Screen */ 55 | 56 | QFrame#GreeterMessage { 57 | padding: 30px; 58 | } 59 | 60 | QFrame#GreeterMessage QLabel#title { 61 | font-size: 100px; 62 | margin: 0; 63 | padding: 0; 64 | qproperty-alignment: AlignCenter; 65 | } 66 | 67 | QFrame#GreeterMessage QPushButton#button { 68 | border: none; 69 | font-size: 80px; 70 | margin: 0; 71 | min-height: 100px; 72 | padding: 0; 73 | text-align: center; 74 | } 75 | 76 | QFrame#GreeterMessage QLabel#message { 77 | font-size: 80px; 78 | margin: 0; 79 | padding: 0; 80 | qproperty-alignment: AlignCenter; 81 | } 82 | 83 | /* Countdown Screen */ 84 | 85 | QFrame#CountdownMessage { 86 | background-color: #eeeeee; 87 | border-style: outset; 88 | border-width: 2px; 89 | border-radius: 30px; 90 | border-color: #eeeeee; 91 | margin: 20px; 92 | padding: 30px; 93 | } 94 | 95 | /* Pose Screen */ 96 | 97 | QFrame#PoseMessage { 98 | background-image: url(photobooth/gui/Qt5Gui/images/camera-800x600.png); 99 | background-repeat: no-repeat; 100 | padding: 280px 80px 80px 80px; 101 | } 102 | 103 | QFrame#PoseMessage QLabel { 104 | font-size: 120px; 105 | qproperty-alignment: AlignCenter; 106 | } 107 | 108 | /* Wait Screen */ 109 | 110 | QFrame#WaitMessage { 111 | padding: 350px 80px 80px 80px; 112 | } 113 | 114 | QFrame#WaitMessage QLabel { 115 | font-size: 70px; 116 | qproperty-alignment: AlignCenter; 117 | } 118 | 119 | /* Picture Screen */ 120 | 121 | QFrame#PictureMessage { 122 | margin: 30px; 123 | } 124 | 125 | /* Overlay message */ 126 | 127 | QWidget#TransparentOverlay { 128 | background-color: #aaeeeeee; 129 | border-style: outset; 130 | border-width: 2px; 131 | border-radius: 30px; 132 | border-color: #eeeeee; 133 | color: #333333; 134 | padding: 40px; 135 | } 136 | 137 | /* Postprocess message */ 138 | 139 | QWidget#PostprocessMessage QLabel { 140 | color: #333333; 141 | font-size: 110px; 142 | qproperty-alignment: AlignCenter; 143 | } 144 | 145 | QWidget#PostprocessMessage QPushButton { 146 | color: #333333; 147 | border-color: #333333; 148 | margin: 20px; 149 | } 150 | 151 | QWidget#PostprocessMessage QPushButton:pressed { 152 | background-color: #66eeeeee; 153 | } 154 | 155 | QWidget#PostprocessMessage QPushButton:disabled { 156 | background-color: #66eeeeee; 157 | color: #33eeeeee; 158 | border-color: #33eeeeee; 159 | } 160 | 161 | /* Customizing settings */ 162 | 163 | QTabWidget::pane { 164 | background-color: #eeeeee; 165 | border-style: outset; 166 | border-width: 1px; 167 | border-radius: 15px; 168 | border-color: #eeeeee; 169 | color: #eeeeee; 170 | padding: 10px; 171 | } 172 | 173 | QTabWidget::tab-bar { 174 | alignment: center; 175 | } 176 | 177 | QTabBar::tab { 178 | background-color: transparent; 179 | border-style: outset; 180 | border-width: 2px; 181 | border-top-left-radius: 15px; 182 | border-top-right-radius: 15px; 183 | border-color: #eeeeee; 184 | color: #eeeeee; 185 | padding: 8px; 186 | } 187 | 188 | QTabBar::tab:selected { 189 | background-color: #33ffffff; 190 | } 191 | 192 | QGroupBox { 193 | background-color: transparent; 194 | border-style: outset; 195 | border-width: 1px; 196 | border-radius: 15px; 197 | border-color: #eeeeee; 198 | margin: 0px; 199 | padding: 4px; 200 | } 201 | 202 | QTabWidget QWidget { 203 | color: #333333; 204 | font-size: 30px; 205 | } 206 | 207 | QCheckBox::indicator { 208 | width: 30px; 209 | height: 30px; 210 | background-color: transparent; 211 | border-style: outset; 212 | border-width: 2px; 213 | border-radius: 5px; 214 | border-color: #333333; 215 | } 216 | 217 | QCheckBox::indicator::checked { 218 | background-image: url(photobooth/gui/Qt5Gui/images/checkmark.png); 219 | background-repeat: no-repeat; 220 | } 221 | 222 | QComboBox, QDateEdit, QLineEdit, QSpinBox, QTimeEdit { 223 | background-color: #eeeeee; 224 | color: #333333; 225 | } 226 | 227 | QComboBox QAbstractItemView { 228 | background-color: #cccccc; 229 | color: #333333; 230 | selection-background-color: #eeeeee; 231 | selection-color: #333333; 232 | } 233 | 234 | QComboBox QAbstractItemView::item { 235 | margin: 5px; 236 | min-height: 50px; 237 | } 238 | -------------------------------------------------------------------------------- /photobooth/gui/Qt5Gui/stylesheets/dark-1024x600.qss: -------------------------------------------------------------------------------- 1 | /* Outer items */ 2 | 3 | QWidget { 4 | background-color: transparent; 5 | color: #eeeeee; 6 | font-family: AmaticSC, sans-serif; 7 | font-size: 50px; 8 | } 9 | 10 | QMainWindow { 11 | background: #000000; 12 | color: #eeeeee; 13 | } 14 | 15 | /* General controls */ 16 | 17 | QPushButton { 18 | background-color: transparent; 19 | border-style: outset; 20 | border-width: 1px; 21 | border-radius: 15px; 22 | border-color: #eeeeee; 23 | padding: 10px; 24 | } 25 | 26 | QPushButton:pressed { 27 | background-color: #66eeeeee; 28 | } 29 | 30 | /* Idle Screen */ 31 | 32 | QFrame#IdleMessage { 33 | background-image: url(photobooth/gui/Qt5Gui/images/arrow-1024x600.png); 34 | background-repeat: no-repeat; 35 | padding: 80px 400px 120px 80px; 36 | } 37 | 38 | QFrame#IdleMessage QLabel { 39 | font-size: 160px; 40 | qproperty-alignment: AlignCenter; 41 | } 42 | 43 | QFrame#IdleMessage QPushButton { 44 | border: none; 45 | color: rgba(255, 27, 0, 200); 46 | font-size: 200px; 47 | text-align: center; 48 | } 49 | 50 | QFrame#IdleMessage QPushButton:pressed { 51 | background-color: rgba(255, 27, 0, 200); 52 | color: #eeeeee; 53 | } 54 | 55 | /* Greeter Screen */ 56 | 57 | QFrame#GreeterMessage { 58 | padding: 30px; 59 | } 60 | 61 | QFrame#GreeterMessage QLabel#title { 62 | font-size: 180px; 63 | margin: 0; 64 | padding: 0; 65 | qproperty-alignment: AlignCenter; 66 | } 67 | 68 | QFrame#GreeterMessage QPushButton#button { 69 | border: none; 70 | font-size: 120px; 71 | margin: 0; 72 | min-height: 160px; 73 | padding: 0; 74 | text-align: center; 75 | } 76 | 77 | QFrame#GreeterMessage QLabel#message { 78 | font-size: 120px; 79 | margin: 0; 80 | padding: 0; 81 | qproperty-alignment: AlignCenter; 82 | } 83 | 84 | /* Countdown Screen */ 85 | 86 | QFrame#CountdownMessage { 87 | background-color: #eeeeee; 88 | border-style: outset; 89 | border-width: 2px; 90 | border-radius: 30px; 91 | border-color: #eeeeee; 92 | margin: 20px; 93 | padding: 30px; 94 | } 95 | 96 | /* Pose Screen */ 97 | 98 | QFrame#PoseMessage { 99 | background-image: url(photobooth/gui/Qt5Gui/images/camera-1024x600.png); 100 | background-repeat: no-repeat; 101 | padding: 380px 80px 80px 80px; 102 | } 103 | 104 | QFrame#PoseMessage QLabel { 105 | font-size: 120px; 106 | qproperty-alignment: AlignCenter; 107 | } 108 | 109 | /* Wait Screen */ 110 | 111 | QFrame#WaitMessage { 112 | padding: 350px 80px 80px 80px; 113 | } 114 | 115 | QFrame#WaitMessage QLabel { 116 | font-size: 110px; 117 | qproperty-alignment: AlignCenter; 118 | } 119 | 120 | /* Picture Screen */ 121 | 122 | QFrame#PictureMessage { 123 | margin: 30px; 124 | } 125 | 126 | /* Overlay message */ 127 | 128 | QWidget#TransparentOverlay { 129 | background-color: #aaeeeeee; 130 | border-style: outset; 131 | border-width: 2px; 132 | border-radius: 30px; 133 | border-color: #eeeeee; 134 | color: #333333; 135 | padding: 40px; 136 | } 137 | 138 | /* Postprocess message */ 139 | 140 | QWidget#PostprocessMessage QLabel { 141 | color: #333333; 142 | font-size: 110px; 143 | qproperty-alignment: AlignCenter; 144 | } 145 | 146 | QWidget#PostprocessMessage QPushButton { 147 | color: #333333; 148 | border-color: #333333; 149 | margin: 20px; 150 | } 151 | 152 | QWidget#PostprocessMessage QPushButton:pressed { 153 | background-color: #66eeeeee; 154 | } 155 | 156 | QWidget#PostprocessMessage QPushButton:disabled { 157 | background-color: #66eeeeee; 158 | color: #33eeeeee; 159 | border-color: #33eeeeee; 160 | } 161 | 162 | /* Customizing settings */ 163 | 164 | QTabWidget::pane { 165 | background-color: #eeeeee; 166 | border-style: outset; 167 | border-width: 1px; 168 | border-radius: 15px; 169 | border-color: #eeeeee; 170 | color: #eeeeee; 171 | padding: 10px; 172 | } 173 | 174 | QTabWidget::tab-bar { 175 | alignment: center; 176 | } 177 | 178 | QTabBar::tab { 179 | background-color: transparent; 180 | border-style: outset; 181 | border-width: 2px; 182 | border-top-left-radius: 15px; 183 | border-top-right-radius: 15px; 184 | border-color: #eeeeee; 185 | color: #eeeeee; 186 | padding: 8px; 187 | } 188 | 189 | QTabBar::tab:selected { 190 | background-color: #33ffffff; 191 | } 192 | 193 | QGroupBox { 194 | background-color: transparent; 195 | border-style: outset; 196 | border-width: 1px; 197 | border-radius: 15px; 198 | border-color: #eeeeee; 199 | margin: 0px; 200 | padding: 4px; 201 | } 202 | 203 | QTabWidget QWidget { 204 | color: #333333; 205 | font-size: 30px; 206 | } 207 | 208 | QCheckBox::indicator { 209 | width: 30px; 210 | height: 30px; 211 | background-color: transparent; 212 | border-style: outset; 213 | border-width: 2px; 214 | border-radius: 5px; 215 | border-color: #333333; 216 | } 217 | 218 | QCheckBox::indicator::checked { 219 | background-image: url(photobooth/gui/Qt5Gui/images/checkmark.png); 220 | background-repeat: no-repeat; 221 | } 222 | 223 | QComboBox, QDateEdit, QLineEdit, QSpinBox, QTimeEdit { 224 | background-color: #eeeeee; 225 | color: #333333; 226 | } 227 | 228 | QComboBox QAbstractItemView { 229 | background-color: #cccccc; 230 | color: #333333; 231 | selection-background-color: #eeeeee; 232 | selection-color: #333333; 233 | } 234 | 235 | QComboBox QAbstractItemView::item { 236 | margin: 5px; 237 | min-height: 50px; 238 | } 239 | -------------------------------------------------------------------------------- /supplementals/Canon_SELPHY_CP1300.ppd: -------------------------------------------------------------------------------- 1 | *PPD-Adobe: "4.3" 2 | *% PPD created by ipp2ppd (v2:Feb 1 2018) 3 | *FormatVersion: "4.3" 4 | *FileVersion: "2.0" 5 | *LanguageVersion: English 6 | *LanguageEncoding: ISOLatin1 7 | *PSVersion: "(3010.000) 0" 8 | *LanguageLevel: "3" 9 | *FileSystem: False 10 | *cupsVersion: 2.0 11 | *cupsModelNumber: 0 12 | *cupsSNMPSupplies: False 13 | *APAirPrint: True 14 | *APAirPrintVersion: 1.4 15 | *APURFVersion: 1.4 16 | *APPrinterFWVersion: "IPP:2.0" 17 | *cupsLanguages: "en" 18 | *cupsIdentifyActions: "flash" 19 | *APAcceptsMixedURF: True 20 | *PCFileName: "APCANONS.PPD" 21 | *ModelName: "Canon SELPHY CP1300 HTTP" 22 | *Product: (Canon SELPHY CP1300 HTTP) 23 | *ShortNickName: "Canon SELPHY CP1300 HTTP" 24 | *Manufacturer: "Canon" 25 | *NickName: "Canon SELPHY CP1300 HTTP-AirPrint" 26 | *Throughput: 1 27 | *APSupplies: "http://192.168.1.108:8008/supply/ink_content.html" 28 | *cupsSingleFile: True 29 | *cupsFilter2: "image/urf image/urf 10 -" 30 | *cupsFilter: "image/urf 10 -" 31 | *cupsFilter2: "image/jpeg image/jpeg 0 maxsize(33554432) -" 32 | *cupsFilter: "image/jpeg 0 -" 33 | *cupsMaxCopies: 99 34 | *ColorDevice: True 35 | *APColorSpaces: true 36 | *DefaultColorSpace: RGB 37 | *%cupsICCProfile Gray../Grayscale: "/System/Library/ColorSync/Profiles/Generic Gray Gamma 2.2 Profile.icc" 38 | *%cupsICCProfile RGB../Color: "/System/Library/ColorSync/Profiles/sRGB Profile.icc" 39 | *OpenUI *ColorModel/Color Mode: PickOne 40 | *OrderDependency: 10 AnySetup *ColorModel 41 | *DefaultColorModel: RGB 42 | *ColorModel Gray/Grayscale: "<>setpagedevice" 43 | *ColorModel RGB/Color: "<>setpagedevice" 44 | *CloseUI: *ColorModel 45 | *LandscapeOrientation: Minus90 46 | *DefaultResolution: 300x300dpi 47 | *OpenUI *cupsPrintQuality/Quality: PickOne 48 | *OrderDependency: 10 AnySetup *cupsPrintQuality 49 | *DefaultcupsPrintQuality: Normal 50 | *cupsPrintQuality Normal: "<>setpagedevice" 51 | *CloseUI: *cupsPrintQuality 52 | *cupsBackSide: Normal 53 | *DefaultOutputOrder: Reverse 54 | *APHasMediaReady: True 55 | *OpenUI *PageSize/Media Size: PickOne 56 | *OrderDependency: 10 AnySetup *PageSize 57 | *DefaultPageSize: Postcard 58 | *PageSize 54x86mm: "<>setpagedevice" 59 | *PageSize 54x86mm.Fullbleed: "<>setpagedevice" 60 | *PageSize 89x119mm: "<>setpagedevice" 61 | *PageSize 89x119mm.Fullbleed: "<>setpagedevice" 62 | *PageSize Postcard: "<>setpagedevice" 63 | *PageSize Postcard.Fullbleed: "<>setpagedevice" 64 | *CloseUI: *PageSize 65 | *OpenUI *PageRegion/Media Size: PickOne 66 | *OrderDependency: 10 AnySetup *PageRegion 67 | *DefaultPageRegion: Postcard 68 | *PageRegion 54x86mm: "<>setpagedevice" 69 | *PageRegion 54x86mm.Fullbleed: "<>setpagedevice" 70 | *PageRegion 89x119mm: "<>setpagedevice" 71 | *PageRegion 89x119mm.Fullbleed: "<>setpagedevice" 72 | *PageRegion Postcard: "<>setpagedevice" 73 | *PageRegion Postcard.Fullbleed: "<>setpagedevice" 74 | *CloseUI: *PageRegion 75 | *DefaultImageableArea: Postcard 76 | *DefaultPaperDimension: Postcard 77 | *PaperDimension 54x86mm: "153.0709 243.7795" 78 | *ImageableArea 54x86mm: "7.0866 18.1417 145.9843 225.6378" 79 | *PaperDimension 54x86mm.Fullbleed: "153.0709 243.7795" 80 | *ImageableArea 54x86mm.Fullbleed: "0.0000 0.0000 153.0709 243.7795" 81 | *PaperDimension 89x119mm: "252.2835 337.3228" 82 | *ImageableArea 89x119mm: "7.0866 9.6378 245.1969 327.6850" 83 | *PaperDimension 89x119mm.Fullbleed: "252.2835 337.3228" 84 | *ImageableArea 89x119mm.Fullbleed: "0.0000 0.0000 252.2835 337.3228" 85 | *PaperDimension Postcard: "283.4646 419.5276" 86 | *ImageableArea Postcard: "7.0866 10.4882 276.3780 409.0394" 87 | *PaperDimension Postcard.Fullbleed: "283.4646 419.5276" 88 | *ImageableArea Postcard.Fullbleed: "0.0000 0.0000 283.4646 419.5276" 89 | *ParamCustomPageSize Width: 1 points 150 289 90 | *ParamCustomPageSize Height: 2 points 241 434 91 | *ParamCustomPageSize WidthOffset: 3 points 0 0 92 | *ParamCustomPageSize HeightOffset: 4 points 0 0 93 | *ParamCustomPageSize Orientation: 5 int 0 3 94 | *CustomPageSize True: "" 95 | *OpenUI *MediaType/MediaType: PickOne 96 | *OrderDependency: 10 AnySetup *MediaType 97 | *MediaType photographic/Photo: "" 98 | *MediaType any/Any: "" 99 | *DefaultMediaType: any 100 | *CloseUI: *MediaType 101 | *APPrinterPreset Gray_with_Paper_Auto-Detect/Black and White: " 102 | *cupsPrintQuality Normal 103 | *ColorModel Gray 104 | com.apple.print.preset.graphicsType General 105 | com.apple.print.preset.quality mid 106 | com.apple.print.preset.media-front-coating autodetect 107 | com.apple.print.preset.output-mode monochrome" 108 | *End 109 | *APPrinterPreset Color_with_Paper_Auto-Detect/Color: " 110 | *cupsPrintQuality Normal 111 | *ColorModel RGB 112 | com.apple.print.preset.graphicsType General 113 | com.apple.print.preset.quality mid 114 | com.apple.print.preset.media-front-coating autodetect" 115 | *End 116 | -------------------------------------------------------------------------------- /photobooth/gui/Qt5Gui/stylesheets/pastel-800x600.qss: -------------------------------------------------------------------------------- 1 | /* Outer items */ 2 | 3 | QWidget { 4 | background-color: transparent; 5 | color: #eeeeee; 6 | font-family: AmaticSC, sans-serif; 7 | font-size: 30px; 8 | } 9 | 10 | QMainWindow { 11 | background: #ffffff qlineargradient(x1:0 y1:1, x2:1, y2:0, stop:0 rgba(255,165,150,255), stop:1 rgba(0,228,255,112)); 12 | color: #eeeeee; 13 | } 14 | 15 | /* General controls */ 16 | 17 | QPushButton { 18 | background-color: transparent; 19 | border-style: outset; 20 | border-width: 1px; 21 | border-radius: 8px; 22 | border-color: #eeeeee; 23 | padding: 8px; 24 | } 25 | 26 | QPushButton:pressed { 27 | background-color: #66eeeeee; 28 | } 29 | 30 | /* Idle Screen */ 31 | 32 | QFrame#IdleMessage { 33 | background-image: url(photobooth/gui/Qt5Gui/images/arrow-800x600.png); 34 | background-repeat: no-repeat; 35 | padding: 80px 40px 60px 40px; 36 | } 37 | 38 | QFrame#IdleMessage QLabel { 39 | font-size: 80px; 40 | qproperty-alignment: AlignCenter; 41 | } 42 | 43 | QFrame#IdleMessage QPushButton { 44 | color: rgba(255, 27, 0, 200); 45 | font-size: 120px; 46 | text-align: center; 47 | } 48 | 49 | QFrame#IdleMessage QPushButton:pressed { 50 | background-color: rgba(255, 27, 0, 200); 51 | color: #eeeeee; 52 | } 53 | 54 | /* Greeter Screen */ 55 | 56 | QFrame#GreeterMessage { 57 | padding: 30px; 58 | } 59 | 60 | QFrame#GreeterMessage QLabel#title { 61 | font-size: 100px; 62 | margin: 0; 63 | padding: 0; 64 | qproperty-alignment: AlignCenter; 65 | } 66 | 67 | QFrame#GreeterMessage QPushButton#button { 68 | border: none; 69 | font-size: 80px; 70 | margin: 0; 71 | min-height: 100px; 72 | padding: 0; 73 | text-align: center; 74 | } 75 | 76 | QFrame#GreeterMessage QLabel#message { 77 | font-size: 80px; 78 | margin: 0; 79 | padding: 0; 80 | qproperty-alignment: AlignCenter; 81 | } 82 | 83 | /* Countdown Screen */ 84 | 85 | QFrame#CountdownMessage { 86 | background-color: #eeeeee; 87 | border-style: outset; 88 | border-width: 2px; 89 | border-radius: 30px; 90 | border-color: #eeeeee; 91 | margin: 20px; 92 | padding: 30px; 93 | } 94 | 95 | /* Pose Screen */ 96 | 97 | QFrame#PoseMessage { 98 | background-image: url(photobooth/gui/Qt5Gui/images/camera-800x600.png); 99 | background-repeat: no-repeat; 100 | padding: 280px 80px 80px 80px; 101 | } 102 | 103 | QFrame#PoseMessage QLabel { 104 | font-size: 120px; 105 | qproperty-alignment: AlignCenter; 106 | } 107 | 108 | /* Wait Screen */ 109 | 110 | QFrame#WaitMessage { 111 | padding: 350px 80px 80px 80px; 112 | } 113 | 114 | QFrame#WaitMessage QLabel { 115 | font-size: 70px; 116 | qproperty-alignment: AlignCenter; 117 | } 118 | 119 | /* Picture Screen */ 120 | 121 | QFrame#PictureMessage { 122 | margin: 30px; 123 | } 124 | 125 | /* Overlay message */ 126 | 127 | QWidget#TransparentOverlay { 128 | background-color: #aaeeeeee; 129 | border-style: outset; 130 | border-width: 2px; 131 | border-radius: 30px; 132 | border-color: #eeeeee; 133 | color: #333333; 134 | padding: 40px; 135 | } 136 | 137 | /* Postprocess message */ 138 | 139 | QWidget#PostprocessMessage QLabel { 140 | color: #333333; 141 | font-size: 110px; 142 | qproperty-alignment: AlignCenter; 143 | } 144 | 145 | QWidget#PostprocessMessage QPushButton { 146 | color: #333333; 147 | border-color: #333333; 148 | background-color: #cceeeeee; 149 | margin: 20px; 150 | } 151 | 152 | QWidget#PostprocessMessage QPushButton:pressed { 153 | background-color: #66eeeeee; 154 | } 155 | 156 | QWidget#PostprocessMessage QPushButton:disabled { 157 | background-color: #33eeeeee; 158 | color: #11eeeeee; 159 | border-color: #11eeeeee; 160 | } 161 | 162 | /* Customizing settings */ 163 | 164 | QTabWidget::pane { 165 | background-color: #eeeeee; 166 | border-style: outset; 167 | border-width: 1px; 168 | border-radius: 15px; 169 | border-color: #eeeeee; 170 | color: #333333; 171 | padding: 10px; 172 | margin: 0; 173 | } 174 | 175 | QTabWidget::tab-bar { 176 | alignment: center; 177 | } 178 | 179 | QTabBar::tab { 180 | background-color: transparent; 181 | border-style: outset; 182 | border-width: 2px; 183 | border-top-left-radius: 15px; 184 | border-top-right-radius: 15px; 185 | border-color: #eeeeee; 186 | padding: 8px; 187 | } 188 | 189 | QTabBar::tab:selected { 190 | background-color: #66ffffff; 191 | } 192 | 193 | QGroupBox { 194 | background-color: transparent; 195 | border-style: outset; 196 | border-width: 1px; 197 | border-radius: 15px; 198 | border-color: #eeeeee; 199 | margin: 0px; 200 | padding: 4px; 201 | } 202 | 203 | QTabWidget QWidget { 204 | color: #333333; 205 | font-size: 30px; 206 | } 207 | 208 | QCheckBox::indicator { 209 | width: 30px; 210 | height: 30px; 211 | background-color: transparent; 212 | border-style: outset; 213 | border-width: 2px; 214 | border-radius: 5px; 215 | border-color: #333333; 216 | } 217 | 218 | QCheckBox::indicator::checked { 219 | background-image: url(photobooth/gui/Qt5Gui/images/checkmark.png); 220 | background-repeat: no-repeat; 221 | } 222 | 223 | QComboBox, QDateEdit, QLineEdit, QSpinBox, QTimeEdit { 224 | background-color: #eeeeee; 225 | color: #333333; 226 | font-size: 30px; 227 | } 228 | 229 | QComboBox QAbstractItemView { 230 | background-color: #cccccc; 231 | color: #333333; 232 | selection-background-color: #eeeeee; 233 | selection-color: #333333; 234 | } 235 | 236 | QComboBox QAbstractItemView::item { 237 | margin: 5px; 238 | min-height: 50px; 239 | } 240 | -------------------------------------------------------------------------------- /photobooth/gui/Qt5Gui/stylesheets/pastel-1024x600.qss: -------------------------------------------------------------------------------- 1 | /* Outer items */ 2 | 3 | QWidget { 4 | background-color: transparent; 5 | color: #eeeeee; 6 | font-family: AmaticSC, sans-serif; 7 | font-size: 50px; 8 | } 9 | 10 | QMainWindow { 11 | background: #ffffff qlineargradient(x1:0 y1:1, x2:1, y2:0, stop:0 rgba(255,165,150,255), stop:1 rgba(0,228,255,112)); 12 | color: #eeeeee; 13 | } 14 | 15 | /* General controls */ 16 | 17 | QPushButton { 18 | background-color: transparent; 19 | border-style: outset; 20 | border-width: 1px; 21 | border-radius: 15px; 22 | border-color: #eeeeee; 23 | padding: 10px; 24 | } 25 | 26 | QPushButton:pressed { 27 | background-color: #66eeeeee; 28 | } 29 | 30 | /* Idle Screen */ 31 | 32 | QFrame#IdleMessage { 33 | background-image: url(photobooth/gui/Qt5Gui/images/arrow-1024x600.png); 34 | background-repeat: no-repeat; 35 | padding: 80px 400px 120px 80px; 36 | } 37 | 38 | QFrame#IdleMessage QLabel { 39 | font-size: 160px; 40 | qproperty-alignment: AlignCenter; 41 | } 42 | 43 | QFrame#IdleMessage QPushButton { 44 | border: none; 45 | color: rgba(255, 27, 0, 200); 46 | font-size: 200px; 47 | text-align: center; 48 | } 49 | 50 | QFrame#IdleMessage QPushButton:pressed { 51 | background-color: rgba(255, 27, 0, 200); 52 | color: #eeeeee; 53 | } 54 | 55 | /* Greeter Screen */ 56 | 57 | QFrame#GreeterMessage { 58 | padding: 30px; 59 | } 60 | 61 | QFrame#GreeterMessage QLabel#title { 62 | font-size: 180px; 63 | margin: 0; 64 | padding: 0; 65 | qproperty-alignment: AlignCenter; 66 | } 67 | 68 | QFrame#GreeterMessage QPushButton#button { 69 | border: none; 70 | font-size: 120px; 71 | margin: 0; 72 | min-height: 160px; 73 | padding: 0; 74 | text-align: center; 75 | } 76 | 77 | QFrame#GreeterMessage QLabel#message { 78 | font-size: 120px; 79 | margin: 0; 80 | padding: 0; 81 | qproperty-alignment: AlignCenter; 82 | } 83 | 84 | /* Countdown Screen */ 85 | 86 | QFrame#CountdownMessage { 87 | background-color: #eeeeee; 88 | border-style: outset; 89 | border-width: 2px; 90 | border-radius: 30px; 91 | border-color: #eeeeee; 92 | margin: 20px; 93 | padding: 30px; 94 | } 95 | 96 | /* Pose Screen */ 97 | 98 | QFrame#PoseMessage { 99 | background-image: url(photobooth/gui/Qt5Gui/images/camera-1024x600.png); 100 | background-repeat: no-repeat; 101 | padding: 380px 80px 80px 80px; 102 | } 103 | 104 | QFrame#PoseMessage QLabel { 105 | font-size: 120px; 106 | qproperty-alignment: AlignCenter; 107 | } 108 | 109 | /* Wait Screen */ 110 | 111 | QFrame#WaitMessage { 112 | padding: 350px 80px 80px 80px; 113 | } 114 | 115 | QFrame#WaitMessage QLabel { 116 | font-size: 110px; 117 | qproperty-alignment: AlignCenter; 118 | } 119 | 120 | /* Picture Screen */ 121 | 122 | QFrame#PictureMessage { 123 | margin: 30px; 124 | } 125 | 126 | /* Overlay message */ 127 | 128 | QWidget#TransparentOverlay { 129 | background-color: #aaeeeeee; 130 | border-style: outset; 131 | border-width: 2px; 132 | border-radius: 30px; 133 | border-color: #eeeeee; 134 | color: #333333; 135 | padding: 40px; 136 | } 137 | 138 | /* Postprocess message */ 139 | 140 | QWidget#PostprocessMessage QLabel { 141 | color: #333333; 142 | font-size: 110px; 143 | qproperty-alignment: AlignCenter; 144 | } 145 | 146 | QWidget#PostprocessMessage QPushButton { 147 | color: #333333; 148 | border-color: #333333; 149 | background-color: #cceeeeee; 150 | margin: 20px; 151 | } 152 | 153 | QWidget#PostprocessMessage QPushButton:pressed { 154 | background-color: #66eeeeee; 155 | } 156 | 157 | QWidget#PostprocessMessage QPushButton:disabled { 158 | background-color: #33eeeeee; 159 | color: #11eeeeee; 160 | border-color: #11eeeeee; 161 | } 162 | 163 | /* Customizing settings */ 164 | 165 | QTabWidget::pane { 166 | background-color: #eeeeee; 167 | border-style: outset; 168 | border-width: 1px; 169 | border-radius: 15px; 170 | border-color: #eeeeee; 171 | color: #333333; 172 | padding: 10px; 173 | margin: 0; 174 | } 175 | 176 | QTabWidget::tab-bar { 177 | alignment: center; 178 | } 179 | 180 | QTabBar::tab { 181 | background-color: transparent; 182 | border-style: outset; 183 | border-width: 2px; 184 | border-top-left-radius: 15px; 185 | border-top-right-radius: 15px; 186 | border-color: #eeeeee; 187 | padding: 8px; 188 | } 189 | 190 | QTabBar::tab:selected { 191 | background-color: #66ffffff; 192 | } 193 | 194 | QGroupBox { 195 | background-color: transparent; 196 | border-style: outset; 197 | border-width: 1px; 198 | border-radius: 15px; 199 | border-color: #eeeeee; 200 | margin: 0px; 201 | padding: 4px; 202 | } 203 | 204 | QTabWidget QWidget { 205 | color: #333333; 206 | font-size: 30px; 207 | } 208 | 209 | QCheckBox::indicator { 210 | width: 30px; 211 | height: 30px; 212 | background-color: transparent; 213 | border-style: outset; 214 | border-width: 2px; 215 | border-radius: 5px; 216 | border-color: #333333; 217 | } 218 | 219 | QCheckBox::indicator::checked { 220 | background-image: url(photobooth/gui/Qt5Gui/images/checkmark.png); 221 | background-repeat: no-repeat; 222 | } 223 | 224 | QComboBox, QDateEdit, QLineEdit, QSpinBox, QTimeEdit { 225 | background-color: #eeeeee; 226 | color: #333333; 227 | font-size: 30px; 228 | } 229 | 230 | QComboBox QAbstractItemView { 231 | background-color: #cccccc; 232 | color: #333333; 233 | selection-background-color: #eeeeee; 234 | selection-color: #333333; 235 | } 236 | 237 | QComboBox QAbstractItemView::item { 238 | margin: 5px; 239 | min-height: 50px; 240 | } 241 | -------------------------------------------------------------------------------- /INSTALL.md: -------------------------------------------------------------------------------- 1 | # Installation instructions 2 | 3 | These instructions are tailored towards running the photobooth on a Raspberry Pi (tested on 1B+ and 3B+). 4 | However, I use my standard Ubuntu Laptop (18.04) with the built-in webcam and OpenCV for development and as such, the app should work on any other hardware just as well. 5 | Simply skip the Raspberry Pi specific installation parts. 6 | 7 | ## Install Raspbian and configure it 8 | 9 | This is just for my own reference and maybe useful, if you have a similar hardware setup. 10 | Skip this, if you have your hardware already up and running. 11 | 12 | ### Install Raspbian Desktop 13 | Choose Raspbian Desktop instead of the Lite flavor, which lacks some packages required for the GUI. 14 | 15 | Download and installation instructions are available at the [Raspberry Pi website](https://www.raspberrypi.org/documentation/installation/installing-images/) 16 | 17 | ### Configure and update Raspbian 18 | Boot up the Raspberry Pi for the first time and open a terminal (press Ctrl+Alt+T). 19 | Enter the following to update everything to the latest version: 20 | 21 | ```bash 22 | sudo rpi-update 23 | sudo apt update 24 | sudo apt dist-upgrade 25 | ``` 26 | 27 | Afterwards, open the configuration utility to adapt everything to your needs (e.g., setup WiFi, hostname, etc.) 28 | ```bash 29 | sudo rpi-config 30 | ``` 31 | 32 | ### Disable screensaver/screen blanking 33 | By default, Raspbian blanks the screen after ten minutes of idle time. 34 | You probably do not want that for a photobooth, thus it is best to disable this. 35 | 36 | For that, edit `/etc/lightdm/lightdm.conf` and change the startup command to the following: 37 | ``` 38 | xserver-command=X -s 0 -dpms 39 | ``` 40 | 41 | ### Configure touch screen, printer etc. 42 | Configure any not working hardware, e.g., my touch screen needs some additional steps since some of the latest Raspbian releases. 43 | See the instructions at the end for my hardware setup. 44 | 45 | If you plan on using a printer, make sure it is configured as default printer! 46 | 47 | 48 | ## Install dependencies for the photobooth 49 | 50 | These dependencies are required to run the application. 51 | You might be able to skip some packages if you plan on not using gphoto2. 52 | 53 | ### Install required packages 54 | In a terminal, enter the following commands 55 | ```bash 56 | sudo apt install python3-dev python3-pip virtualenv 57 | sudo apt install qt5-default pyqt5-dev pyqt5-dev-tools # for PyQt5-GUI 58 | sudo apt install gphoto2 libgphoto2-dev # to use gphoto2 59 | sudo apt install libcups2-dev # to use pycups 60 | ``` 61 | 62 | If you want to use the gphoto2-cffi bindings you have to install the following packages: 63 | ```bash 64 | sudo apt install libffi6 libffi-dev # for gphoto2-cffi bindings 65 | ``` 66 | 67 | ### Remove some files to get gphoto2 working 68 | Raspbian ships with a utility called `gvfs` to allow mounting cameras as virtual file systems. 69 | This enables you to access some camera models as if they were USB storage drives, however, it interferes with our use of the camera, as the operating system then claims exclusive access to the camera. 70 | Thus, we have to disable these functionalities. 71 | 72 | *Note: This might break file manager access etc. for some camera models.* 73 | 74 | To remove these files, enter the following in a terminal: 75 | ```bash 76 | sudo rm /usr/share/dbus-1/services/org.gtk.vfs.GPhoto2VolumeMonitor.service 77 | sudo rm /usr/share/gvfs/mounts/gphoto2.mount 78 | sudo rm /usr/share/gvfs/remote-volume-monitors/gphoto2.mount 79 | sudo rm /usr/lib/gvfs/gvfs-gphoto2-volume-monitor 80 | sudo rm /usr/lib/gvfs/gvfsd-gphoto2 81 | ``` 82 | 83 | You should reboot afterwards to make sure these changes are effective. 84 | 85 | ## Install photobooth 86 | 87 | These are the steps to install the application. 88 | 89 | ### Clone the Photobooth repository 90 | Run the following command to obtain the source code: 91 | ```bash 92 | git clone https://github.com/reuterbal/photobooth.git 93 | ``` 94 | This will create a folder `photobooth` with all necessary files. 95 | 96 | ### Initialize `virtualenv` 97 | To avoid installing everything on a system level, I recommend to initialize a virtual environment. 98 | For that, enter the folder created in the previous step 99 | ```bash 100 | cd photobooth 101 | ``` 102 | and run the following command 103 | ```bash 104 | virtualenv -p python3 --system-site-packages .venv 105 | ``` 106 | Activate the virtual environment. 107 | You have to do this whenever you open a new terminal or rebooted your hardware 108 | ```bash 109 | source .venv/bin/activate 110 | ``` 111 | 112 | ### Install photobooth with dependencies 113 | Run the following command to download and install all dependencies and the photobooth: 114 | ```bash 115 | pip install -e . 116 | ``` 117 | 118 | Some dependencies are optional and must be included explicitly if you plan on using them. 119 | For that, change the above command to (note the lack of a whitespace after the dot) 120 | ```bash 121 | pip install -e .[extras] 122 | ``` 123 | and replace `extras` by a comma separated list (without whitespaces!) of the desired options. 124 | These include: 125 | - `pyqt` if you want to install PyQt5 from PIP (doesn't work on Raspbian) 126 | - `picamera` if you want to use the Raspberry Pi camera module 127 | - `gphoto2-cffi` if you want to use the `gphoto2-cffi` bindings 128 | 129 | ## Run Photobooth 130 | If not yet done, activate your virtual environment 131 | ```bash 132 | source .venv/bin/activate 133 | ``` 134 | and run the photobooth as 135 | ```bash 136 | python -m photobooth 137 | ``` 138 | 139 | Alternatively, use the Python binary of the virtual environment to start the photobooth directly without activating the environment first: 140 | ```bash 141 | .venv/bin/python -m photobooth 142 | ``` 143 | This is useful, e.g., when starting the photobooth from scripts, desktop shortcuts, or when using an autostart mechanism of your window manager. 144 | 145 | Change any settings via the "Settings" menu. 146 | Afterwards, select "Start photobooth" to get started. 147 | You can trigger the countdown via space bar or an external button. 148 | 149 | To exit the application, use the Esc-key or an external button. 150 | 151 | You can directly startup the photobooth to the idle screen (skipping the welcome screen) by appending the parameter `--run`. 152 | -------------------------------------------------------------------------------- /photobooth/camera/__init__.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | 4 | # Photobooth - a flexible photo booth software 5 | # Copyright (C) 2018 Balthasar Reuter 6 | # 7 | # This program is free software: you can redistribute it and/or modify 8 | # it under the terms of the GNU Affero General Public License as published by 9 | # the Free Software Foundation, either version 3 of the License, or 10 | # (at your option) any later version. 11 | # 12 | # This program is distributed in the hope that it will be useful, 13 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 14 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 15 | # GNU Affero General Public License for more details. 16 | # 17 | # You should have received a copy of the GNU Affero General Public License 18 | # along with this program. If not, see . 19 | 20 | import logging 21 | 22 | from PIL import Image, ImageOps 23 | from io import BytesIO 24 | 25 | from .PictureDimensions import PictureDimensions 26 | from .. import StateMachine 27 | from ..Threading import Workers 28 | 29 | # Available camera modules as tuples of (config name, module name, class name) 30 | modules = ( 31 | ('python-gphoto2', 'CameraGphoto2', 'CameraGphoto2'), 32 | ('gphoto2-cffi', 'CameraGphoto2Cffi', 'CameraGphoto2Cffi'), 33 | ('gphoto2-commandline', 'CameraGphoto2CommandLine', 34 | 'CameraGphoto2CommandLine'), 35 | ('opencv', 'CameraOpenCV', 'CameraOpenCV'), 36 | ('picamera', 'CameraPicamera', 'CameraPicamera'), 37 | ('dummy', 'CameraDummy', 'CameraDummy')) 38 | 39 | 40 | class Camera: 41 | 42 | def __init__(self, config, comm, CameraModule): 43 | 44 | super().__init__() 45 | 46 | self._comm = comm 47 | self._cfg = config 48 | self._cam = CameraModule 49 | 50 | self._cap = None 51 | self._pic_dims = None 52 | 53 | self._is_preview = self._cfg.getBool('Photobooth', 'show_preview') 54 | self._is_keep_pictures = self._cfg.getBool('Storage', 'keep_pictures') 55 | 56 | rot_vals = {0: None, 90: Image.ROTATE_90, 180: Image.ROTATE_180, 57 | 270: Image.ROTATE_270} 58 | self._rotation = rot_vals[self._cfg.getInt('Camera', 'rotation')] 59 | 60 | def startup(self): 61 | 62 | self._cap = self._cam() 63 | 64 | logging.info('Using camera {} preview functionality'.format( 65 | 'with' if self._is_preview else 'without')) 66 | 67 | test_picture = self._cap.getPicture() 68 | if self._rotation is not None: 69 | test_picture = test_picture.transpose(self._rotation) 70 | 71 | self._pic_dims = PictureDimensions(self._cfg, test_picture.size) 72 | self._is_preview = self._is_preview and self._cap.hasPreview 73 | 74 | background = self._cfg.get('Picture', 'background') 75 | if len(background) > 0: 76 | logging.info('Using background "{}"'.format(background)) 77 | bg_picture = Image.open(background) 78 | self._template = bg_picture.resize(self._pic_dims.outputSize) 79 | else: 80 | self._template = Image.new('RGB', self._pic_dims.outputSize, 81 | (255, 255, 255)) 82 | 83 | self.setIdle() 84 | self._comm.send(Workers.MASTER, StateMachine.CameraEvent('ready')) 85 | 86 | def teardown(self, state): 87 | 88 | if self._cap is not None: 89 | self._cap.cleanup() 90 | 91 | def run(self): 92 | 93 | for state in self._comm.iter(Workers.CAMERA): 94 | self.handleState(state) 95 | 96 | return True 97 | 98 | def handleState(self, state): 99 | 100 | if isinstance(state, StateMachine.StartupState): 101 | self.startup() 102 | elif isinstance(state, StateMachine.GreeterState): 103 | self.prepareCapture() 104 | elif isinstance(state, StateMachine.CountdownState): 105 | self.capturePreview() 106 | elif isinstance(state, StateMachine.CaptureState): 107 | self.capturePicture(state) 108 | elif isinstance(state, StateMachine.AssembleState): 109 | self.assemblePicture() 110 | elif isinstance(state, StateMachine.TeardownState): 111 | self.teardown(state) 112 | 113 | def setActive(self): 114 | 115 | self._cap.setActive() 116 | 117 | def setIdle(self): 118 | 119 | if self._cap.hasIdle: 120 | self._cap.setIdle() 121 | 122 | def prepareCapture(self): 123 | 124 | self.setActive() 125 | self._pictures = [] 126 | 127 | def capturePreview(self): 128 | 129 | if self._is_preview: 130 | while self._comm.empty(Workers.CAMERA): 131 | picture = self._cap.getPreview() 132 | if self._rotation is not None: 133 | picture = picture.transpose(self._rotation) 134 | picture = picture.resize(self._pic_dims.previewSize) 135 | picture = ImageOps.mirror(picture) 136 | byte_data = BytesIO() 137 | picture.save(byte_data, format='jpeg') 138 | self._comm.send(Workers.GUI, 139 | StateMachine.CameraEvent('preview', byte_data)) 140 | 141 | def capturePicture(self, state): 142 | 143 | self.setIdle() 144 | picture = self._cap.getPicture() 145 | if self._rotation is not None: 146 | picture = picture.transpose(self._rotation) 147 | byte_data = BytesIO() 148 | picture.save(byte_data, format='jpeg') 149 | self._pictures.append(byte_data) 150 | self.setActive() 151 | 152 | if self._is_keep_pictures: 153 | self._comm.send(Workers.WORKER, 154 | StateMachine.CameraEvent('capture', byte_data)) 155 | 156 | if state.num_picture < self._pic_dims.totalNumPictures: 157 | self._comm.send(Workers.MASTER, 158 | StateMachine.CameraEvent('countdown')) 159 | else: 160 | self._comm.send(Workers.MASTER, 161 | StateMachine.CameraEvent('assemble')) 162 | 163 | def assemblePicture(self): 164 | 165 | self.setIdle() 166 | 167 | picture = self._template.copy() 168 | for i in range(self._pic_dims.totalNumPictures): 169 | shot = Image.open(self._pictures[i]) 170 | resized = shot.resize(self._pic_dims.thumbnailSize) 171 | picture.paste(resized, self._pic_dims.thumbnailOffset[i]) 172 | 173 | byte_data = BytesIO() 174 | picture.save(byte_data, format='jpeg') 175 | self._comm.send(Workers.MASTER, 176 | StateMachine.CameraEvent('review', byte_data)) 177 | self._pictures = [] 178 | -------------------------------------------------------------------------------- /photobooth/locale/messages.pot: -------------------------------------------------------------------------------- 1 | # Translations template for photobooth. 2 | # Copyright (C) 2018 ORGANIZATION 3 | # This file is distributed under the same license as the photobooth project. 4 | # FIRST AUTHOR , 2018. 5 | # 6 | #, fuzzy 7 | msgid "" 8 | msgstr "" 9 | "Project-Id-Version: photobooth 0.4.dev48+g8241189.d20180823\n" 10 | "Report-Msgid-Bugs-To: EMAIL@ADDRESS\n" 11 | "POT-Creation-Date: 2018-08-23 16:13+0200\n" 12 | "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" 13 | "Last-Translator: FULL NAME \n" 14 | "Language-Team: LANGUAGE \n" 15 | "MIME-Version: 1.0\n" 16 | "Content-Type: text/plain; charset=utf-8\n" 17 | "Content-Transfer-Encoding: 8bit\n" 18 | "Generated-By: Babel 2.6.0\n" 19 | 20 | #: photobooth/gui/Qt5Gui/Frames.py:50 21 | msgid "Start photobooth" 22 | msgstr "" 23 | 24 | #: photobooth/gui/Qt5Gui/Frames.py:53 25 | msgid "Set date/time" 26 | msgstr "" 27 | 28 | #: photobooth/gui/Qt5Gui/Frames.py:56 29 | msgid "Settings" 30 | msgstr "" 31 | 32 | #: photobooth/gui/Qt5Gui/Frames.py:59 33 | msgid "Quit" 34 | msgstr "" 35 | 36 | #: photobooth/gui/Qt5Gui/Frames.py:68 37 | msgid "photobooth" 38 | msgstr "" 39 | 40 | #: photobooth/gui/Qt5Gui/Frames.py:87 41 | msgid "Hit the" 42 | msgstr "" 43 | 44 | #: photobooth/gui/Qt5Gui/Frames.py:88 45 | msgid "Button!" 46 | msgstr "" 47 | 48 | #: photobooth/gui/Qt5Gui/Frames.py:111 49 | msgid "Get ready!" 50 | msgstr "" 51 | 52 | #: photobooth/gui/Qt5Gui/Frames.py:112 53 | msgid "Start countdown" 54 | msgstr "" 55 | 56 | #: photobooth/gui/Qt5Gui/Frames.py:116 57 | msgid "for {} pictures..." 58 | msgstr "" 59 | 60 | #: photobooth/gui/Qt5Gui/Frames.py:148 61 | msgid "Picture {} of {}..." 62 | msgstr "" 63 | 64 | #: photobooth/gui/Qt5Gui/Frames.py:342 65 | msgid "Start over" 66 | msgstr "" 67 | 68 | #: photobooth/gui/Qt5Gui/Frames.py:351 69 | msgid "Happy?" 70 | msgstr "" 71 | 72 | #: photobooth/gui/Qt5Gui/Frames.py:383 73 | msgid "Date:" 74 | msgstr "" 75 | 76 | #: photobooth/gui/Qt5Gui/Frames.py:384 77 | msgid "Time:" 78 | msgstr "" 79 | 80 | #: photobooth/gui/Qt5Gui/Frames.py:387 81 | msgid "Set system date and time:" 82 | msgstr "" 83 | 84 | #: photobooth/gui/Qt5Gui/Frames.py:396 photobooth/gui/Qt5Gui/Frames.py:486 85 | msgid "Save and restart" 86 | msgstr "" 87 | 88 | #: photobooth/gui/Qt5Gui/Frames.py:400 photobooth/gui/Qt5Gui/Frames.py:490 89 | msgid "Cancel" 90 | msgstr "" 91 | 92 | #: photobooth/gui/Qt5Gui/Frames.py:472 93 | msgid "Interface" 94 | msgstr "" 95 | 96 | #: photobooth/gui/Qt5Gui/Frames.py:473 97 | msgid "Photobooth" 98 | msgstr "" 99 | 100 | #: photobooth/gui/Qt5Gui/Frames.py:474 101 | msgid "Camera" 102 | msgstr "" 103 | 104 | #: photobooth/gui/Qt5Gui/Frames.py:475 105 | msgid "Picture" 106 | msgstr "" 107 | 108 | #: photobooth/gui/Qt5Gui/Frames.py:476 109 | msgid "Storage" 110 | msgstr "" 111 | 112 | #: photobooth/gui/Qt5Gui/Frames.py:477 113 | msgid "GPIO" 114 | msgstr "" 115 | 116 | #: photobooth/gui/Qt5Gui/Frames.py:478 117 | msgid "Printer" 118 | msgstr "" 119 | 120 | #: photobooth/gui/Qt5Gui/Frames.py:494 121 | msgid "Restore defaults" 122 | msgstr "" 123 | 124 | #: photobooth/gui/Qt5Gui/Frames.py:553 125 | msgid "Enable fullscreen:" 126 | msgstr "" 127 | 128 | #: photobooth/gui/Qt5Gui/Frames.py:554 129 | msgid "Gui module:" 130 | msgstr "" 131 | 132 | #: photobooth/gui/Qt5Gui/Frames.py:555 133 | msgid "Window size [px]:" 134 | msgstr "" 135 | 136 | #: photobooth/gui/Qt5Gui/Frames.py:556 137 | msgid "Hide cursor:" 138 | msgstr "" 139 | 140 | #: photobooth/gui/Qt5Gui/Frames.py:557 141 | msgid "Appearance:" 142 | msgstr "" 143 | 144 | #: photobooth/gui/Qt5Gui/Frames.py:597 145 | msgid "Show preview during countdown:" 146 | msgstr "" 147 | 148 | #: photobooth/gui/Qt5Gui/Frames.py:598 149 | msgid "Greeter time before countdown [s]:" 150 | msgstr "" 151 | 152 | #: photobooth/gui/Qt5Gui/Frames.py:599 153 | msgid "Countdown time [s]:" 154 | msgstr "" 155 | 156 | #: photobooth/gui/Qt5Gui/Frames.py:600 157 | msgid "Picture display time [s]:" 158 | msgstr "" 159 | 160 | #: photobooth/gui/Qt5Gui/Frames.py:601 161 | msgid "Postprocess timeout [s]:" 162 | msgstr "" 163 | 164 | #: photobooth/gui/Qt5Gui/Frames.py:602 165 | msgid "Overwrite displayed error message:" 166 | msgstr "" 167 | 168 | #: photobooth/gui/Qt5Gui/Frames.py:633 169 | msgid "Camera module:" 170 | msgstr "" 171 | 172 | #: photobooth/gui/Qt5Gui/Frames.py:634 173 | msgid "Camera rotation:" 174 | msgstr "" 175 | 176 | #: photobooth/gui/Qt5Gui/Frames.py:698 photobooth/gui/Qt5Gui/Frames.py:701 177 | msgid "Select file" 178 | msgstr "" 179 | 180 | #: photobooth/gui/Qt5Gui/Frames.py:709 181 | msgid "Number of shots per picture:" 182 | msgstr "" 183 | 184 | #: photobooth/gui/Qt5Gui/Frames.py:710 185 | msgid "Size of assembled picture [px]:" 186 | msgstr "" 187 | 188 | #: photobooth/gui/Qt5Gui/Frames.py:711 189 | msgid "Min. distance between shots [px]:" 190 | msgstr "" 191 | 192 | #: photobooth/gui/Qt5Gui/Frames.py:712 193 | msgid "Omit last picture:" 194 | msgstr "" 195 | 196 | #: photobooth/gui/Qt5Gui/Frames.py:713 197 | msgid "Background image:" 198 | msgstr "" 199 | 200 | #: photobooth/gui/Qt5Gui/Frames.py:734 photobooth/gui/Qt5Gui/Frames.py:738 201 | msgid "Select directory" 202 | msgstr "" 203 | 204 | #: photobooth/gui/Qt5Gui/Frames.py:746 205 | msgid "Output directory (strftime possible):" 206 | msgstr "" 207 | 208 | #: photobooth/gui/Qt5Gui/Frames.py:747 209 | msgid "Basename of files (strftime possible):" 210 | msgstr "" 211 | 212 | #: photobooth/gui/Qt5Gui/Frames.py:748 213 | msgid "Keep single shots:" 214 | msgstr "" 215 | 216 | #: photobooth/gui/Qt5Gui/Frames.py:798 217 | msgid "Enable GPIO:" 218 | msgstr "" 219 | 220 | #: photobooth/gui/Qt5Gui/Frames.py:799 221 | msgid "Exit button pin (BCM numbering):" 222 | msgstr "" 223 | 224 | #: photobooth/gui/Qt5Gui/Frames.py:800 225 | msgid "Trigger button pin (BCM numbering):" 226 | msgstr "" 227 | 228 | #: photobooth/gui/Qt5Gui/Frames.py:801 229 | msgid "Idle lamp pin (BCM numbering):" 230 | msgstr "" 231 | 232 | #: photobooth/gui/Qt5Gui/Frames.py:802 233 | msgid "RGB LED pins (BCM numbering):" 234 | msgstr "" 235 | 236 | #: photobooth/gui/Qt5Gui/Frames.py:843 237 | msgid "Enable printing:" 238 | msgstr "" 239 | 240 | #: photobooth/gui/Qt5Gui/Frames.py:844 241 | msgid "Module:" 242 | msgstr "" 243 | 244 | #: photobooth/gui/Qt5Gui/Frames.py:845 245 | msgid "Print to PDF (for debugging):" 246 | msgstr "" 247 | 248 | #: photobooth/gui/Qt5Gui/Frames.py:846 249 | msgid "Ask for confirmation before printing:" 250 | msgstr "" 251 | 252 | #: photobooth/gui/Qt5Gui/Frames.py:847 253 | msgid "Paper size [mm]:" 254 | msgstr "" 255 | 256 | #: photobooth/gui/Qt5Gui/PyQt5Gui.py:164 257 | msgid "Starting the photobooth..." 258 | msgstr "" 259 | 260 | #: photobooth/gui/Qt5Gui/PyQt5Gui.py:214 261 | msgid "Processing picture..." 262 | msgstr "" 263 | 264 | #: photobooth/gui/Qt5Gui/PyQt5Gui.py:286 265 | msgid "Confirmation" 266 | msgstr "" 267 | 268 | #: photobooth/gui/Qt5Gui/PyQt5Gui.py:287 269 | msgid "Quit Photobooth?" 270 | msgstr "" 271 | 272 | -------------------------------------------------------------------------------- /photobooth/gui/Qt5Gui/Widgets.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | 4 | # Photobooth - a flexible photo booth software 5 | # Copyright (C) 2018 Balthasar Reuter 6 | # 7 | # This program is free software: you can redistribute it and/or modify 8 | # it under the terms of the GNU Affero General Public License as published by 9 | # the Free Software Foundation, either version 3 of the License, or 10 | # (at your option) any later version. 11 | # 12 | # This program is distributed in the hope that it will be useful, 13 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 14 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 15 | # GNU Affero General Public License for more details. 16 | # 17 | # You should have received a copy of the GNU Affero General Public License 18 | # along with this program. If not, see . 19 | 20 | import math 21 | 22 | from PyQt5 import Qt 23 | from PyQt5 import QtCore 24 | from PyQt5 import QtGui 25 | from PyQt5 import QtWidgets 26 | 27 | 28 | class SpinningWaitClock(QtWidgets.QWidget): 29 | # Spinning wait clock, inspired by 30 | # https://wiki.python.org/moin/PyQt/A%20full%20widget%20waiting%20indicator 31 | 32 | def __init__(self): 33 | 34 | super().__init__() 35 | 36 | self._num_dots = 8 37 | self._value = 0 38 | 39 | @property 40 | def value(self): 41 | 42 | return self._value 43 | 44 | @value.setter 45 | def value(self, value): 46 | 47 | if self._value != value: 48 | self._value = value 49 | self.update() 50 | 51 | def showEvent(self, event): 52 | 53 | self.startTimer(100) 54 | 55 | def timerEvent(self, event): 56 | 57 | self.value += 1 58 | 59 | def paintEvent(self, event): 60 | 61 | painter = QtGui.QPainter(self) 62 | painter.setRenderHint(QtGui.QPainter.Antialiasing) 63 | painter.setPen(QtGui.QPen(QtCore.Qt.NoPen)) 64 | 65 | dots = self._num_dots 66 | center = (self.width() / 2, self.height() / 2) 67 | pos = self.value % dots 68 | 69 | for dot in range(dots): 70 | distance = (pos - dot) % dots 71 | offset = (180 / dots * math.cos(2 * math.pi * dot / dots) - 20, 72 | 180 / dots * math.sin(2 * math.pi * dot / dots) - 20) 73 | 74 | color = (distance + 1) / (dots + 1) * 255 75 | painter.setBrush(QtGui.QBrush(QtGui.QColor(color, color, color))) 76 | 77 | painter.drawEllipse(center[0] + offset[0], center[1] + offset[1], 78 | 15, 15) 79 | 80 | painter.end() 81 | 82 | 83 | class RoundProgressBar(QtWidgets.QWidget): 84 | # Adaptation of QRoundProgressBar from 85 | # https://sourceforge.net/projects/qroundprogressbar/ 86 | # to PyQt5, using the PyQt4-version offered at 87 | # https://stackoverflow.com/a/33583019 88 | 89 | def __init__(self, begin, end, value): 90 | 91 | super().__init__() 92 | 93 | self._begin = begin 94 | self._end = end 95 | self._value = value 96 | 97 | self._data_pen_width = 7 98 | self._outline_pen_width = 10 99 | self._null_position = 90 100 | 101 | @property 102 | def value(self): 103 | 104 | return self._value 105 | 106 | @value.setter 107 | def value(self, value): 108 | 109 | if self._value != value: 110 | if value < self._begin: 111 | self._value = self._begin 112 | elif value > self._end: 113 | self._value = self._end 114 | else: 115 | self._value = value 116 | 117 | def _drawBase(self, painter, base_rect): 118 | 119 | color = self.palette().base().color() 120 | color.setAlpha(100) 121 | brush = self.palette().base() 122 | brush.setColor(color) 123 | painter.setPen(QtGui.QPen(self.palette().base().color(), 124 | self._outline_pen_width)) 125 | painter.setBrush(brush) 126 | 127 | painter.drawEllipse(base_rect.adjusted(self._outline_pen_width // 2, 128 | self._outline_pen_width // 2, 129 | -self._outline_pen_width // 2, 130 | -self._outline_pen_width // 2)) 131 | 132 | def _drawCircle(self, painter, base_rect): 133 | 134 | if self.value == self._begin: 135 | return 136 | 137 | arc_length = 360 / (self._end - self._begin) * self.value 138 | 139 | painter.setPen(QtGui.QPen(self.palette().text().color(), 140 | self._data_pen_width)) 141 | painter.setBrush(Qt.Qt.NoBrush) 142 | painter.drawArc(base_rect.adjusted(self._outline_pen_width // 2, 143 | self._outline_pen_width // 2, 144 | -self._outline_pen_width // 2, 145 | -self._outline_pen_width // 2), 146 | self._null_position * 16, -arc_length * 16) 147 | 148 | def _drawText(self, painter, inner_rect, inner_radius): 149 | 150 | text = '{}'.format(math.ceil(self.value)) 151 | 152 | f = self.font() 153 | f.setPixelSize(inner_radius * 0.8 / len(text)) 154 | painter.setFont(f) 155 | painter.setPen(self.palette().text().color()) 156 | 157 | painter.drawText(inner_rect, Qt.Qt.AlignCenter, text) 158 | 159 | def paintEvent(self, event): 160 | 161 | outer_radius = min(self.width(), self.height()) 162 | inner_radius = outer_radius - self._outline_pen_width 163 | delta = (outer_radius - inner_radius) / 2 164 | 165 | base_rect = QtCore.QRectF(1, 1, outer_radius - 2, outer_radius - 2) 166 | inner_rect = QtCore.QRectF(delta, delta, inner_radius, inner_radius) 167 | 168 | painter = QtGui.QPainter(self) 169 | painter.setRenderHint(QtGui.QPainter.Antialiasing) 170 | 171 | # base circle 172 | self._drawBase(painter, base_rect) 173 | 174 | # data circle 175 | self._drawCircle(painter, base_rect) 176 | 177 | # text 178 | self._drawText(painter, inner_rect, inner_radius) 179 | 180 | painter.end() 181 | 182 | 183 | class TransparentOverlay(QtWidgets.QWidget): 184 | 185 | def __init__(self, parent, timeout=None, timeout_handle=None): 186 | 187 | super().__init__(parent) 188 | self.setObjectName('TransparentOverlay') 189 | 190 | rect = parent.rect() 191 | rect.adjust(50, 50, -50, -50) 192 | self.setGeometry(rect) 193 | 194 | if timeout is not None: 195 | self._handle = timeout_handle 196 | self._timer = self.startTimer(timeout) 197 | 198 | self.show() 199 | 200 | def paintEvent(self, event): 201 | 202 | opt = QtWidgets.QStyleOption() 203 | opt.initFrom(self) 204 | painter = QtGui.QPainter(self) 205 | self.style().drawPrimitive(QtWidgets.QStyle.PE_Widget, opt, painter, 206 | self) 207 | painter.end() 208 | 209 | def timerEvent(self, event): 210 | 211 | self.killTimer(self._timer) 212 | self._handle() 213 | -------------------------------------------------------------------------------- /supplementals/housing/Displayframe.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 20 | 22 | 25 | 29 | 30 | 31 | 49 | 51 | 52 | 54 | image/svg+xml 55 | 57 | 58 | 59 | 60 | 61 | 66 | 70 | 77 | 84 | 85 | 86 | 91 | 98 | 105 | 112 | 119 | 120 | 125 | 132 | 139 | 146 | 153 | 154 | 159 | 166 | 173 | 174 | 179 | 188 | 195 | 202 | 203 | 204 | -------------------------------------------------------------------------------- /photobooth/locale/en/LC_MESSAGES/photobooth.po: -------------------------------------------------------------------------------- 1 | msgid "" 2 | msgstr "" 3 | "MIME-Version: 1.0\n" 4 | "Content-Type: text/plain; charset=UTF-8\n" 5 | "Content-Transfer-Encoding: 8bit\n" 6 | "X-Generator: POEditor.com\n" 7 | "Project-Id-Version: Photobooth\n" 8 | "Language: en\n" 9 | 10 | #: photobooth/gui/Qt5Gui/Frames.py:50 11 | msgid "Start photobooth" 12 | msgstr "Start photobooth" 13 | 14 | #: photobooth/gui/Qt5Gui/Frames.py:53 15 | msgid "Set date/time" 16 | msgstr "Set date/time" 17 | 18 | #: photobooth/gui/Qt5Gui/Frames.py:56 19 | msgid "Settings" 20 | msgstr "Settings" 21 | 22 | #: photobooth/gui/Qt5Gui/Frames.py:59 23 | msgid "Quit" 24 | msgstr "Quit" 25 | 26 | #: photobooth/gui/Qt5Gui/Frames.py:68 27 | msgid "photobooth" 28 | msgstr "photobooth" 29 | 30 | #: photobooth/gui/Qt5Gui/Frames.py:87 31 | msgid "Hit the" 32 | msgstr "Hit the" 33 | 34 | #: photobooth/gui/Qt5Gui/Frames.py:88 35 | msgid "Button!" 36 | msgstr "Button!" 37 | 38 | #: photobooth/gui/Qt5Gui/Frames.py:111 39 | msgid "Get ready!" 40 | msgstr "Get ready!" 41 | 42 | #: photobooth/gui/Qt5Gui/Frames.py:112 43 | msgid "Start countdown" 44 | msgstr "Start countdown" 45 | 46 | #: photobooth/gui/Qt5Gui/Frames.py:116 47 | msgid "for {} pictures..." 48 | msgstr "for {} pictures..." 49 | 50 | #: photobooth/gui/Qt5Gui/Frames.py:148 51 | msgid "Picture {} of {}..." 52 | msgstr "Picture {} of {}..." 53 | 54 | #: photobooth/gui/Qt5Gui/Frames.py:342 55 | msgid "Start over" 56 | msgstr "Start over" 57 | 58 | #: photobooth/gui/Qt5Gui/Frames.py:351 59 | msgid "Happy?" 60 | msgstr "Happy?" 61 | 62 | #: photobooth/gui/Qt5Gui/Frames.py:383 63 | msgid "Date:" 64 | msgstr "Date:" 65 | 66 | #: photobooth/gui/Qt5Gui/Frames.py:384 67 | msgid "Time:" 68 | msgstr "Time:" 69 | 70 | #: photobooth/gui/Qt5Gui/Frames.py:387 71 | msgid "Set system date and time:" 72 | msgstr "Set system date and time:" 73 | 74 | #: photobooth/gui/Qt5Gui/Frames.py:396 photobooth/gui/Qt5Gui/Frames.py:486 75 | msgid "Save and restart" 76 | msgstr "Save and restart" 77 | 78 | #: photobooth/gui/Qt5Gui/Frames.py:400 photobooth/gui/Qt5Gui/Frames.py:490 79 | msgid "Cancel" 80 | msgstr "Cancel" 81 | 82 | #: photobooth/gui/Qt5Gui/Frames.py:472 83 | msgid "Interface" 84 | msgstr "Interface" 85 | 86 | #: photobooth/gui/Qt5Gui/Frames.py:473 87 | msgid "Photobooth" 88 | msgstr "Photobooth" 89 | 90 | #: photobooth/gui/Qt5Gui/Frames.py:474 91 | msgid "Camera" 92 | msgstr "Camera" 93 | 94 | #: photobooth/gui/Qt5Gui/Frames.py:475 95 | msgid "Picture" 96 | msgstr "Picture" 97 | 98 | #: photobooth/gui/Qt5Gui/Frames.py:476 99 | msgid "Storage" 100 | msgstr "Storage" 101 | 102 | #: photobooth/gui/Qt5Gui/Frames.py:477 103 | msgid "GPIO" 104 | msgstr "GPIO" 105 | 106 | #: photobooth/gui/Qt5Gui/Frames.py:478 107 | msgid "Printer" 108 | msgstr "Printer" 109 | 110 | #: photobooth/gui/Qt5Gui/Frames.py:494 111 | msgid "Restore defaults" 112 | msgstr "Restore defaults" 113 | 114 | #: photobooth/gui/Qt5Gui/Frames.py:553 115 | msgid "Enable fullscreen:" 116 | msgstr "Enable fullscreen:" 117 | 118 | #: photobooth/gui/Qt5Gui/Frames.py:554 119 | msgid "Gui module:" 120 | msgstr "Gui module:" 121 | 122 | #: photobooth/gui/Qt5Gui/Frames.py:555 123 | msgid "Window size [px]:" 124 | msgstr "Window size [px]:" 125 | 126 | #: photobooth/gui/Qt5Gui/Frames.py:556 127 | msgid "Hide cursor:" 128 | msgstr "Hide cursor:" 129 | 130 | #: photobooth/gui/Qt5Gui/Frames.py:557 131 | msgid "Appearance:" 132 | msgstr "Appearance:" 133 | 134 | #: photobooth/gui/Qt5Gui/Frames.py:597 135 | msgid "Show preview during countdown:" 136 | msgstr "Show preview during countdown:" 137 | 138 | #: photobooth/gui/Qt5Gui/Frames.py:598 139 | msgid "Greeter time before countdown [s]:" 140 | msgstr "Greeter time before countdown [s]:" 141 | 142 | #: photobooth/gui/Qt5Gui/Frames.py:599 143 | msgid "Countdown time [s]:" 144 | msgstr "Countdown time [s]:" 145 | 146 | #: photobooth/gui/Qt5Gui/Frames.py:600 147 | msgid "Picture display time [s]:" 148 | msgstr "Picture display time [s]:" 149 | 150 | #: photobooth/gui/Qt5Gui/Frames.py:601 151 | msgid "Postprocess timeout [s]:" 152 | msgstr "Postprocess timeout [s]:" 153 | 154 | #: photobooth/gui/Qt5Gui/Frames.py:602 155 | msgid "Overwrite displayed error message:" 156 | msgstr "Overwrite displayed error message:" 157 | 158 | #: photobooth/gui/Qt5Gui/Frames.py:617 159 | msgid "Camera module:" 160 | msgstr "Camera module:" 161 | 162 | #: photobooth/gui/Qt5Gui/Frames.py:681 photobooth/gui/Qt5Gui/Frames.py:684 163 | msgid "Select file" 164 | msgstr "Select file" 165 | 166 | #: photobooth/gui/Qt5Gui/Frames.py:692 167 | msgid "Number of shots per picture:" 168 | msgstr "Number of shots per picture:" 169 | 170 | #: photobooth/gui/Qt5Gui/Frames.py:693 171 | msgid "Size of assembled picture [px]:" 172 | msgstr "Size of assembled picture [px]:" 173 | 174 | #: photobooth/gui/Qt5Gui/Frames.py:694 175 | msgid "Min. distance between shots [px]:" 176 | msgstr "Min. distance between shots [px]:" 177 | 178 | #: photobooth/gui/Qt5Gui/Frames.py:695 179 | msgid "Omit last picture:" 180 | msgstr "Omit last picture:" 181 | 182 | #: photobooth/gui/Qt5Gui/Frames.py:696 183 | msgid "Background image:" 184 | msgstr "Background image:" 185 | 186 | #: photobooth/gui/Qt5Gui/Frames.py:717 photobooth/gui/Qt5Gui/Frames.py:721 187 | msgid "Select directory" 188 | msgstr "Select directory" 189 | 190 | #: photobooth/gui/Qt5Gui/Frames.py:729 191 | msgid "Output directory (strftime possible):" 192 | msgstr "Output directory (strftime possible):" 193 | 194 | #: photobooth/gui/Qt5Gui/Frames.py:730 195 | msgid "Basename of files (strftime possible):" 196 | msgstr "Basename of files (strftime possible):" 197 | 198 | #: photobooth/gui/Qt5Gui/Frames.py:731 199 | msgid "Keep single shots:" 200 | msgstr "Keep single shots:" 201 | 202 | #: photobooth/gui/Qt5Gui/Frames.py:781 203 | msgid "Enable GPIO:" 204 | msgstr "Enable GPIO:" 205 | 206 | #: photobooth/gui/Qt5Gui/Frames.py:782 207 | msgid "Exit button pin (BCM numbering):" 208 | msgstr "Exit button pin (BCM numbering):" 209 | 210 | #: photobooth/gui/Qt5Gui/Frames.py:783 211 | msgid "Trigger button pin (BCM numbering):" 212 | msgstr "Trigger button pin (BCM numbering):" 213 | 214 | #: photobooth/gui/Qt5Gui/Frames.py:784 215 | msgid "Idle lamp pin (BCM numbering):" 216 | msgstr "Idle lamp pin (BCM numbering):" 217 | 218 | #: photobooth/gui/Qt5Gui/Frames.py:785 219 | msgid "RGB LED pins (BCM numbering):" 220 | msgstr "RGB LED pins (BCM numbering):" 221 | 222 | #: photobooth/gui/Qt5Gui/Frames.py:826 223 | msgid "Enable printing:" 224 | msgstr "Enable printing:" 225 | 226 | #: photobooth/gui/Qt5Gui/Frames.py:827 227 | msgid "Module:" 228 | msgstr "Module:" 229 | 230 | #: photobooth/gui/Qt5Gui/Frames.py:828 231 | msgid "Print to PDF (for debugging):" 232 | msgstr "Print to PDF (for debugging):" 233 | 234 | #: photobooth/gui/Qt5Gui/Frames.py:829 235 | msgid "Ask for confirmation before printing:" 236 | msgstr "Ask for confirmation before printing:" 237 | 238 | #: photobooth/gui/Qt5Gui/Frames.py:830 239 | msgid "Paper size [mm]:" 240 | msgstr "Paper size [mm]:" 241 | 242 | #: photobooth/gui/Qt5Gui/PyQt5Gui.py:164 243 | msgid "Starting the photobooth..." 244 | msgstr "Starting the photobooth..." 245 | 246 | #: photobooth/gui/Qt5Gui/PyQt5Gui.py:214 247 | msgid "Processing picture..." 248 | msgstr "Processing picture..." 249 | 250 | #: photobooth/gui/Qt5Gui/PyQt5Gui.py:286 251 | msgid "Confirmation" 252 | msgstr "Confirmation" 253 | 254 | #: photobooth/gui/Qt5Gui/PyQt5Gui.py:287 255 | msgid "Quit Photobooth?" 256 | msgstr "Quit Photobooth?" 257 | 258 | -------------------------------------------------------------------------------- /photobooth/locale/de/LC_MESSAGES/photobooth.po: -------------------------------------------------------------------------------- 1 | msgid "" 2 | msgstr "" 3 | "MIME-Version: 1.0\n" 4 | "Content-Type: text/plain; charset=UTF-8\n" 5 | "Content-Transfer-Encoding: 8bit\n" 6 | "X-Generator: POEditor.com\n" 7 | "Project-Id-Version: Photobooth\n" 8 | "Language: de\n" 9 | 10 | #: photobooth/gui/Qt5Gui/Frames.py:50 11 | msgid "Start photobooth" 12 | msgstr "Photobooth starten" 13 | 14 | #: photobooth/gui/Qt5Gui/Frames.py:53 15 | msgid "Set date/time" 16 | msgstr "Uhrzeit ändern" 17 | 18 | #: photobooth/gui/Qt5Gui/Frames.py:56 19 | msgid "Settings" 20 | msgstr "Einstellungen" 21 | 22 | #: photobooth/gui/Qt5Gui/Frames.py:59 23 | msgid "Quit" 24 | msgstr "Beenden" 25 | 26 | #: photobooth/gui/Qt5Gui/Frames.py:68 27 | msgid "photobooth" 28 | msgstr "Photobooth" 29 | 30 | #: photobooth/gui/Qt5Gui/Frames.py:87 31 | msgid "Hit the" 32 | msgstr "Drück' den" 33 | 34 | #: photobooth/gui/Qt5Gui/Frames.py:88 35 | msgid "Button!" 36 | msgstr "Knopf!" 37 | 38 | #: photobooth/gui/Qt5Gui/Frames.py:111 39 | msgid "Get ready!" 40 | msgstr "Bereit machen!" 41 | 42 | #: photobooth/gui/Qt5Gui/Frames.py:112 43 | msgid "Start countdown" 44 | msgstr "Countdown starten" 45 | 46 | #: photobooth/gui/Qt5Gui/Frames.py:116 47 | msgid "for {} pictures..." 48 | msgstr "für {} Fotos..." 49 | 50 | #: photobooth/gui/Qt5Gui/Frames.py:148 51 | msgid "Picture {} of {}..." 52 | msgstr "Foto {} von {}..." 53 | 54 | #: photobooth/gui/Qt5Gui/Frames.py:342 55 | msgid "Start over" 56 | msgstr "Nochmal" 57 | 58 | #: photobooth/gui/Qt5Gui/Frames.py:351 59 | msgid "Happy?" 60 | msgstr "Zufrieden?" 61 | 62 | #: photobooth/gui/Qt5Gui/Frames.py:383 63 | msgid "Date:" 64 | msgstr "Datum:" 65 | 66 | #: photobooth/gui/Qt5Gui/Frames.py:384 67 | msgid "Time:" 68 | msgstr "Uhrzeit:" 69 | 70 | #: photobooth/gui/Qt5Gui/Frames.py:387 71 | msgid "Set system date and time:" 72 | msgstr "Systemzeit ändern:" 73 | 74 | #: photobooth/gui/Qt5Gui/Frames.py:396 photobooth/gui/Qt5Gui/Frames.py:486 75 | msgid "Save and restart" 76 | msgstr "Speichern & Neustart" 77 | 78 | #: photobooth/gui/Qt5Gui/Frames.py:400 photobooth/gui/Qt5Gui/Frames.py:490 79 | msgid "Cancel" 80 | msgstr "Abbrechen" 81 | 82 | #: photobooth/gui/Qt5Gui/Frames.py:472 83 | msgid "Interface" 84 | msgstr "Oberfläche" 85 | 86 | #: photobooth/gui/Qt5Gui/Frames.py:473 87 | msgid "Photobooth" 88 | msgstr "Photobooth" 89 | 90 | #: photobooth/gui/Qt5Gui/Frames.py:474 91 | msgid "Camera" 92 | msgstr "Kamera" 93 | 94 | #: photobooth/gui/Qt5Gui/Frames.py:475 95 | msgid "Picture" 96 | msgstr "Bild" 97 | 98 | #: photobooth/gui/Qt5Gui/Frames.py:476 99 | msgid "Storage" 100 | msgstr "Speicher" 101 | 102 | #: photobooth/gui/Qt5Gui/Frames.py:477 103 | msgid "GPIO" 104 | msgstr "GPIO" 105 | 106 | #: photobooth/gui/Qt5Gui/Frames.py:478 107 | msgid "Printer" 108 | msgstr "Drucker" 109 | 110 | #: photobooth/gui/Qt5Gui/Frames.py:494 111 | msgid "Restore defaults" 112 | msgstr "Zurücksetzen" 113 | 114 | #: photobooth/gui/Qt5Gui/Frames.py:553 115 | msgid "Enable fullscreen:" 116 | msgstr "Vollbild aktivieren:" 117 | 118 | #: photobooth/gui/Qt5Gui/Frames.py:554 119 | msgid "Gui module:" 120 | msgstr "GUI Modul:" 121 | 122 | #: photobooth/gui/Qt5Gui/Frames.py:555 123 | msgid "Window size [px]:" 124 | msgstr "Fenstergröße [px]:" 125 | 126 | #: photobooth/gui/Qt5Gui/Frames.py:556 127 | msgid "Hide cursor:" 128 | msgstr "Mauszeiger ausblenden:" 129 | 130 | #: photobooth/gui/Qt5Gui/Frames.py:557 131 | msgid "Appearance:" 132 | msgstr "Erscheinungsbild:" 133 | 134 | #: photobooth/gui/Qt5Gui/Frames.py:597 135 | msgid "Show preview during countdown:" 136 | msgstr "Vorschau während Countdown zeigen:" 137 | 138 | #: photobooth/gui/Qt5Gui/Frames.py:598 139 | msgid "Greeter time before countdown [s]:" 140 | msgstr "Wartezeit auf Begrüßungsbildschirm [s]:" 141 | 142 | #: photobooth/gui/Qt5Gui/Frames.py:599 143 | msgid "Countdown time [s]:" 144 | msgstr "Countdown-Dauer [s]:" 145 | 146 | #: photobooth/gui/Qt5Gui/Frames.py:600 147 | msgid "Picture display time [s]:" 148 | msgstr "Anzeigedauer fertiges Bild [s]:" 149 | 150 | #: photobooth/gui/Qt5Gui/Frames.py:601 151 | msgid "Postprocess timeout [s]:" 152 | msgstr "Wartezeit auf Nachbearbeitungsbildschirm [s]" 153 | 154 | #: photobooth/gui/Qt5Gui/Frames.py:602 155 | msgid "Overwrite displayed error message:" 156 | msgstr "Ersetze angezeigte Fehlermeldung:" 157 | 158 | #: photobooth/gui/Qt5Gui/Frames.py:617 159 | msgid "Camera module:" 160 | msgstr "Kamera Modul:" 161 | 162 | #: photobooth/gui/Qt5Gui/Frames.py:681 photobooth/gui/Qt5Gui/Frames.py:684 163 | msgid "Select file" 164 | msgstr "Datei auswählen" 165 | 166 | #: photobooth/gui/Qt5Gui/Frames.py:692 167 | msgid "Number of shots per picture:" 168 | msgstr "Anzahl Fotos pro Bild:" 169 | 170 | #: photobooth/gui/Qt5Gui/Frames.py:693 171 | msgid "Size of assembled picture [px]:" 172 | msgstr "Größe des fertigen Bildes [px]:" 173 | 174 | #: photobooth/gui/Qt5Gui/Frames.py:694 175 | msgid "Min. distance between shots [px]:" 176 | msgstr "Min. Abstand der Fotos im Bild [px]:" 177 | 178 | #: photobooth/gui/Qt5Gui/Frames.py:695 179 | msgid "Omit last picture:" 180 | msgstr "Letztes Foto weg lassen:" 181 | 182 | #: photobooth/gui/Qt5Gui/Frames.py:696 183 | msgid "Background image:" 184 | msgstr "Hintergrundbild:" 185 | 186 | #: photobooth/gui/Qt5Gui/Frames.py:717 photobooth/gui/Qt5Gui/Frames.py:721 187 | msgid "Select directory" 188 | msgstr "Verzeichnis auswählen:" 189 | 190 | #: photobooth/gui/Qt5Gui/Frames.py:729 191 | msgid "Output directory (strftime possible):" 192 | msgstr "Ausgabeverzeichnis (strftime möglich):" 193 | 194 | #: photobooth/gui/Qt5Gui/Frames.py:730 195 | msgid "Basename of files (strftime possible):" 196 | msgstr "Dateiname (strftime möglich):" 197 | 198 | #: photobooth/gui/Qt5Gui/Frames.py:731 199 | msgid "Keep single shots:" 200 | msgstr "Einzelfotos behalten:" 201 | 202 | #: photobooth/gui/Qt5Gui/Frames.py:781 203 | msgid "Enable GPIO:" 204 | msgstr "GPIO aktivieren:" 205 | 206 | #: photobooth/gui/Qt5Gui/Frames.py:782 207 | msgid "Exit button pin (BCM numbering):" 208 | msgstr "Pin für Beenden-Schalter (BCM-Numm.):" 209 | 210 | #: photobooth/gui/Qt5Gui/Frames.py:783 211 | msgid "Trigger button pin (BCM numbering):" 212 | msgstr "Pin für Auslöser (BCM-Numm.):" 213 | 214 | #: photobooth/gui/Qt5Gui/Frames.py:784 215 | msgid "Idle lamp pin (BCM numbering):" 216 | msgstr "Pin für Leerlauf-Lampe (BCM-Numm.):" 217 | 218 | #: photobooth/gui/Qt5Gui/Frames.py:785 219 | msgid "RGB LED pins (BCM numbering):" 220 | msgstr "Pins für RGB-LEDs (BCM-Numm.):" 221 | 222 | #: photobooth/gui/Qt5Gui/Frames.py:826 223 | msgid "Enable printing:" 224 | msgstr "Drucken aktivieren:" 225 | 226 | #: photobooth/gui/Qt5Gui/Frames.py:827 227 | msgid "Module:" 228 | msgstr "Modul:" 229 | 230 | #: photobooth/gui/Qt5Gui/Frames.py:828 231 | msgid "Print to PDF (for debugging):" 232 | msgstr "In PDF drucken (für Debugging):" 233 | 234 | #: photobooth/gui/Qt5Gui/Frames.py:829 235 | msgid "Ask for confirmation before printing:" 236 | msgstr "Bestätigung vor Druckvorgang nötig:" 237 | 238 | #: photobooth/gui/Qt5Gui/Frames.py:830 239 | msgid "Paper size [mm]:" 240 | msgstr "Papiergröße [mm]:" 241 | 242 | #: photobooth/gui/Qt5Gui/PyQt5Gui.py:164 243 | msgid "Starting the photobooth..." 244 | msgstr "Starte die Photobooth..." 245 | 246 | #: photobooth/gui/Qt5Gui/PyQt5Gui.py:214 247 | msgid "Processing picture..." 248 | msgstr "Bild wird erzeugt..." 249 | 250 | #: photobooth/gui/Qt5Gui/PyQt5Gui.py:286 251 | msgid "Confirmation" 252 | msgstr "Bestätigung" 253 | 254 | #: photobooth/gui/Qt5Gui/PyQt5Gui.py:287 255 | msgid "Quit Photobooth?" 256 | msgstr "Photobooth beenden?" 257 | 258 | -------------------------------------------------------------------------------- /photobooth/locale/es/LC_MESSAGES/photobooth.po: -------------------------------------------------------------------------------- 1 | msgid "" 2 | msgstr "" 3 | "MIME-Version: 1.0\n" 4 | "Content-Type: text/plain; charset=UTF-8\n" 5 | "Content-Transfer-Encoding: 8bit\n" 6 | "X-Generator: POEditor.com\n" 7 | "Project-Id-Version: Photobooth\n" 8 | "Language: es\n" 9 | 10 | #: photobooth/gui/Qt5Gui/Frames.py:50 11 | msgid "Start photobooth" 12 | msgstr "Inicia fotocabina" 13 | 14 | #: photobooth/gui/Qt5Gui/Frames.py:53 15 | msgid "Set date/time" 16 | msgstr "Def fecha/hora" 17 | 18 | #: photobooth/gui/Qt5Gui/Frames.py:56 19 | msgid "Settings" 20 | msgstr "Configurar" 21 | 22 | #: photobooth/gui/Qt5Gui/Frames.py:59 23 | msgid "Quit" 24 | msgstr "Salir" 25 | 26 | #: photobooth/gui/Qt5Gui/Frames.py:68 27 | msgid "photobooth" 28 | msgstr "Fotocabina" 29 | 30 | #: photobooth/gui/Qt5Gui/Frames.py:87 31 | msgid "Hit the" 32 | msgstr "Presiona el" 33 | 34 | #: photobooth/gui/Qt5Gui/Frames.py:88 35 | msgid "Button!" 36 | msgstr "Botón" 37 | 38 | #: photobooth/gui/Qt5Gui/Frames.py:111 39 | msgid "Get ready!" 40 | msgstr "Prepárate!" 41 | 42 | #: photobooth/gui/Qt5Gui/Frames.py:112 43 | msgid "Start countdown" 44 | msgstr "Inicia cuenta atrás" 45 | 46 | #: photobooth/gui/Qt5Gui/Frames.py:116 47 | msgid "for {} pictures..." 48 | msgstr "Para {} imágenes..." 49 | 50 | #: photobooth/gui/Qt5Gui/Frames.py:148 51 | msgid "Picture {} of {}..." 52 | msgstr "Imagen {} de {}..." 53 | 54 | #: photobooth/gui/Qt5Gui/Frames.py:342 55 | msgid "Start over" 56 | msgstr "Reiniciar" 57 | 58 | #: photobooth/gui/Qt5Gui/Frames.py:351 59 | msgid "Happy?" 60 | msgstr "Contento?" 61 | 62 | #: photobooth/gui/Qt5Gui/Frames.py:383 63 | msgid "Date:" 64 | msgstr "Fecha:" 65 | 66 | #: photobooth/gui/Qt5Gui/Frames.py:384 67 | msgid "Time:" 68 | msgstr "Hora:" 69 | 70 | #: photobooth/gui/Qt5Gui/Frames.py:387 71 | msgid "Set system date and time:" 72 | msgstr "Pon fecha y hora del sistema" 73 | 74 | #: photobooth/gui/Qt5Gui/Frames.py:396 photobooth/gui/Qt5Gui/Frames.py:486 75 | msgid "Save and restart" 76 | msgstr "Guardar y reiniciar" 77 | 78 | #: photobooth/gui/Qt5Gui/Frames.py:400 photobooth/gui/Qt5Gui/Frames.py:490 79 | msgid "Cancel" 80 | msgstr "Cancelar" 81 | 82 | #: photobooth/gui/Qt5Gui/Frames.py:472 83 | msgid "Interface" 84 | msgstr "Interfaz" 85 | 86 | #: photobooth/gui/Qt5Gui/Frames.py:473 87 | msgid "Photobooth" 88 | msgstr "Fotocabina" 89 | 90 | #: photobooth/gui/Qt5Gui/Frames.py:474 91 | msgid "Camera" 92 | msgstr "Cámara" 93 | 94 | #: photobooth/gui/Qt5Gui/Frames.py:475 95 | msgid "Picture" 96 | msgstr "Imqgen" 97 | 98 | #: photobooth/gui/Qt5Gui/Frames.py:476 99 | msgid "Storage" 100 | msgstr "Almacenamiento" 101 | 102 | #: photobooth/gui/Qt5Gui/Frames.py:477 103 | msgid "GPIO" 104 | msgstr "GPIO" 105 | 106 | #: photobooth/gui/Qt5Gui/Frames.py:478 107 | msgid "Printer" 108 | msgstr "Impresora" 109 | 110 | #: photobooth/gui/Qt5Gui/Frames.py:494 111 | msgid "Restore defaults" 112 | msgstr "Restaurar pretederminados" 113 | 114 | #: photobooth/gui/Qt5Gui/Frames.py:553 115 | msgid "Enable fullscreen:" 116 | msgstr "Habilitar pantalla completa" 117 | 118 | #: photobooth/gui/Qt5Gui/Frames.py:554 119 | msgid "Gui module:" 120 | msgstr "Módulo gui:" 121 | 122 | #: photobooth/gui/Qt5Gui/Frames.py:555 123 | msgid "Window size [px]:" 124 | msgstr "Tamaño ventana [px]:" 125 | 126 | #: photobooth/gui/Qt5Gui/Frames.py:556 127 | msgid "Hide cursor:" 128 | msgstr "Ocultar cursor" 129 | 130 | #: photobooth/gui/Qt5Gui/Frames.py:557 131 | msgid "Appearance:" 132 | msgstr "Apariencia:" 133 | 134 | #: photobooth/gui/Qt5Gui/Frames.py:597 135 | msgid "Show preview during countdown:" 136 | msgstr "Mostrar imagen en la cuenta regresiva:" 137 | 138 | #: photobooth/gui/Qt5Gui/Frames.py:598 139 | msgid "Greeter time before countdown [s]:" 140 | msgstr "Tiempo muerto antes de cuenta regresiva [s]:" 141 | 142 | #: photobooth/gui/Qt5Gui/Frames.py:599 143 | msgid "Countdown time [s]:" 144 | msgstr "Duración cuenta regresiva [s]:" 145 | 146 | #: photobooth/gui/Qt5Gui/Frames.py:600 147 | msgid "Picture display time [s]:" 148 | msgstr "Tiempo de muestra imagen [s]:" 149 | 150 | #: photobooth/gui/Qt5Gui/Frames.py:601 151 | msgid "Postprocess timeout [s]:" 152 | msgstr "Tiempo fuera de postproceso [s]:" 153 | 154 | #: photobooth/gui/Qt5Gui/Frames.py:602 155 | msgid "Overwrite displayed error message:" 156 | msgstr "Sobrescribir mensaje de error mostrado:" 157 | 158 | #: photobooth/gui/Qt5Gui/Frames.py:617 159 | msgid "Camera module:" 160 | msgstr "Módulo de cámara:" 161 | 162 | #: photobooth/gui/Qt5Gui/Frames.py:681 photobooth/gui/Qt5Gui/Frames.py:684 163 | msgid "Select file" 164 | msgstr "Seleccione archivo" 165 | 166 | #: photobooth/gui/Qt5Gui/Frames.py:692 167 | msgid "Number of shots per picture:" 168 | msgstr "Número de tomas por imagen:" 169 | 170 | #: photobooth/gui/Qt5Gui/Frames.py:693 171 | msgid "Size of assembled picture [px]:" 172 | msgstr "Tamaño de imagen ensamblada [px]:" 173 | 174 | #: photobooth/gui/Qt5Gui/Frames.py:694 175 | msgid "Min. distance between shots [px]:" 176 | msgstr "Distancia min. entre tomas [px]:" 177 | 178 | #: photobooth/gui/Qt5Gui/Frames.py:695 179 | msgid "Omit last picture:" 180 | msgstr "Omitir último imagen:" 181 | 182 | #: photobooth/gui/Qt5Gui/Frames.py:696 183 | msgid "Background image:" 184 | msgstr "Imagen de fondo:" 185 | 186 | #: photobooth/gui/Qt5Gui/Frames.py:717 photobooth/gui/Qt5Gui/Frames.py:721 187 | msgid "Select directory" 188 | msgstr "Seleccione directorio" 189 | 190 | #: photobooth/gui/Qt5Gui/Frames.py:729 191 | msgid "Output directory (strftime possible):" 192 | msgstr "Directorio de salida (strfhora posible):" 193 | 194 | #: photobooth/gui/Qt5Gui/Frames.py:730 195 | msgid "Basename of files (strftime possible):" 196 | msgstr "Nombre base de archivos (strfhora posible):" 197 | 198 | #: photobooth/gui/Qt5Gui/Frames.py:731 199 | msgid "Keep single shots:" 200 | msgstr "Guardar fotos sueltas:" 201 | 202 | #: photobooth/gui/Qt5Gui/Frames.py:781 203 | msgid "Enable GPIO:" 204 | msgstr "Habilitar GPIO:" 205 | 206 | #: photobooth/gui/Qt5Gui/Frames.py:782 207 | msgid "Exit button pin (BCM numbering):" 208 | msgstr "Pin de botón salir (numeración BCM):" 209 | 210 | #: photobooth/gui/Qt5Gui/Frames.py:783 211 | msgid "Trigger button pin (BCM numbering):" 212 | msgstr "Pon de botón gatillo (numeración BCM ):" 213 | 214 | #: photobooth/gui/Qt5Gui/Frames.py:784 215 | msgid "Idle lamp pin (BCM numbering):" 216 | msgstr "Pin lámpara espera (numeración BCM):" 217 | 218 | #: photobooth/gui/Qt5Gui/Frames.py:785 219 | msgid "RGB LED pins (BCM numbering):" 220 | msgstr "Pins LED RGB (numeración BCM):" 221 | 222 | #: photobooth/gui/Qt5Gui/Frames.py:826 223 | msgid "Enable printing:" 224 | msgstr "Habilitar impresión:" 225 | 226 | #: photobooth/gui/Qt5Gui/Frames.py:827 227 | msgid "Module:" 228 | msgstr "Módulo:" 229 | 230 | #: photobooth/gui/Qt5Gui/Frames.py:828 231 | msgid "Print to PDF (for debugging):" 232 | msgstr "Imprimir a PDF (para debugging):" 233 | 234 | #: photobooth/gui/Qt5Gui/Frames.py:829 235 | msgid "Ask for confirmation before printing:" 236 | msgstr "Pedir confirmar antes de imprimir:" 237 | 238 | #: photobooth/gui/Qt5Gui/Frames.py:830 239 | msgid "Paper size [mm]:" 240 | msgstr "Tamaño de papel [mm]:" 241 | 242 | #: photobooth/gui/Qt5Gui/PyQt5Gui.py:164 243 | msgid "Starting the photobooth..." 244 | msgstr "Iniciando fotocabina..." 245 | 246 | #: photobooth/gui/Qt5Gui/PyQt5Gui.py:214 247 | msgid "Processing picture..." 248 | msgstr "Procesando imagen..." 249 | 250 | #: photobooth/gui/Qt5Gui/PyQt5Gui.py:286 251 | msgid "Confirmation" 252 | msgstr "Confirmar" 253 | 254 | #: photobooth/gui/Qt5Gui/PyQt5Gui.py:287 255 | msgid "Quit Photobooth?" 256 | msgstr "Salir de fotocabina?" 257 | 258 | -------------------------------------------------------------------------------- /photobooth/locale/fr/LC_MESSAGES/photobooth.po: -------------------------------------------------------------------------------- 1 | msgid "" 2 | msgstr "" 3 | "MIME-Version: 1.0\n" 4 | "Content-Type: text/plain; charset=UTF-8\n" 5 | "Content-Transfer-Encoding: 8bit\n" 6 | "X-Generator: POEditor.com\n" 7 | "Project-Id-Version: Photobooth\n" 8 | "Language: fr\n" 9 | 10 | #: photobooth/gui/Qt5Gui/Frames.py:50 11 | msgid "Start photobooth" 12 | msgstr "Démarrer photobooth" 13 | 14 | #: photobooth/gui/Qt5Gui/Frames.py:53 15 | msgid "Set date/time" 16 | msgstr "Définir date/heure" 17 | 18 | #: photobooth/gui/Qt5Gui/Frames.py:56 19 | msgid "Settings" 20 | msgstr "Paramètres" 21 | 22 | #: photobooth/gui/Qt5Gui/Frames.py:59 23 | msgid "Quit" 24 | msgstr "Quitter" 25 | 26 | #: photobooth/gui/Qt5Gui/Frames.py:68 27 | msgid "photobooth" 28 | msgstr "photobooth" 29 | 30 | #: photobooth/gui/Qt5Gui/Frames.py:87 31 | msgid "Hit the" 32 | msgstr "Appuyez sur le" 33 | 34 | #: photobooth/gui/Qt5Gui/Frames.py:88 35 | msgid "Button!" 36 | msgstr "Bouton !" 37 | 38 | #: photobooth/gui/Qt5Gui/Frames.py:111 39 | msgid "Get ready!" 40 | msgstr "Préparez-vous !" 41 | 42 | #: photobooth/gui/Qt5Gui/Frames.py:112 43 | msgid "Start countdown" 44 | msgstr "Démarrage du décompte" 45 | 46 | #: photobooth/gui/Qt5Gui/Frames.py:116 47 | msgid "for {} pictures..." 48 | msgstr "Pour {} photos ..." 49 | 50 | #: photobooth/gui/Qt5Gui/Frames.py:148 51 | msgid "Picture {} of {}..." 52 | msgstr "Photo {} sur {} ..." 53 | 54 | #: photobooth/gui/Qt5Gui/Frames.py:342 55 | msgid "Start over" 56 | msgstr "Recommencer" 57 | 58 | #: photobooth/gui/Qt5Gui/Frames.py:351 59 | msgid "Happy?" 60 | msgstr "Satisfait ?" 61 | 62 | #: photobooth/gui/Qt5Gui/Frames.py:383 63 | msgid "Date:" 64 | msgstr "Date :" 65 | 66 | #: photobooth/gui/Qt5Gui/Frames.py:384 67 | msgid "Time:" 68 | msgstr "Heure :" 69 | 70 | #: photobooth/gui/Qt5Gui/Frames.py:387 71 | msgid "Set system date and time:" 72 | msgstr "Définir la date et l'heure du système" 73 | 74 | #: photobooth/gui/Qt5Gui/Frames.py:396 photobooth/gui/Qt5Gui/Frames.py:486 75 | msgid "Save and restart" 76 | msgstr "Sauvegarde et redémarre" 77 | 78 | #: photobooth/gui/Qt5Gui/Frames.py:400 photobooth/gui/Qt5Gui/Frames.py:490 79 | msgid "Cancel" 80 | msgstr "Annuler" 81 | 82 | #: photobooth/gui/Qt5Gui/Frames.py:472 83 | msgid "Interface" 84 | msgstr "Interface" 85 | 86 | #: photobooth/gui/Qt5Gui/Frames.py:473 87 | msgid "Photobooth" 88 | msgstr "Photobooth" 89 | 90 | #: photobooth/gui/Qt5Gui/Frames.py:474 91 | msgid "Camera" 92 | msgstr "Appareil photo" 93 | 94 | #: photobooth/gui/Qt5Gui/Frames.py:475 95 | msgid "Picture" 96 | msgstr "Photo" 97 | 98 | #: photobooth/gui/Qt5Gui/Frames.py:476 99 | msgid "Storage" 100 | msgstr "Stockage" 101 | 102 | #: photobooth/gui/Qt5Gui/Frames.py:477 103 | msgid "GPIO" 104 | msgstr "GPIO" 105 | 106 | #: photobooth/gui/Qt5Gui/Frames.py:478 107 | msgid "Printer" 108 | msgstr "Imprimante" 109 | 110 | #: photobooth/gui/Qt5Gui/Frames.py:494 111 | msgid "Restore defaults" 112 | msgstr "Restaure les paramètres par défaut" 113 | 114 | #: photobooth/gui/Qt5Gui/Frames.py:553 115 | msgid "Enable fullscreen:" 116 | msgstr "Activer plein écran :" 117 | 118 | #: photobooth/gui/Qt5Gui/Frames.py:554 119 | msgid "Gui module:" 120 | msgstr "Module d'interface graphique" 121 | 122 | #: photobooth/gui/Qt5Gui/Frames.py:555 123 | msgid "Window size [px]:" 124 | msgstr "Taille de la fenêtre [px] :" 125 | 126 | #: photobooth/gui/Qt5Gui/Frames.py:556 127 | msgid "Hide cursor:" 128 | msgstr "Cacher le curseur :" 129 | 130 | #: photobooth/gui/Qt5Gui/Frames.py:557 131 | msgid "Appearance:" 132 | msgstr "Apparence" 133 | 134 | #: photobooth/gui/Qt5Gui/Frames.py:597 135 | msgid "Show preview during countdown:" 136 | msgstr "Afficher l'aperçu durant le décompte" 137 | 138 | #: photobooth/gui/Qt5Gui/Frames.py:598 139 | msgid "Greeter time before countdown [s]:" 140 | msgstr "Temps d'attente avant décompte [s] :" 141 | 142 | #: photobooth/gui/Qt5Gui/Frames.py:599 143 | msgid "Countdown time [s]:" 144 | msgstr "Temps de décompte [s] :" 145 | 146 | #: photobooth/gui/Qt5Gui/Frames.py:600 147 | msgid "Picture display time [s]:" 148 | msgstr "Temps d'affichage de l'image [s] :" 149 | 150 | #: photobooth/gui/Qt5Gui/Frames.py:601 151 | msgid "Postprocess timeout [s]:" 152 | msgstr "Délai de post-traitement" 153 | 154 | #: photobooth/gui/Qt5Gui/Frames.py:602 155 | msgid "Overwrite displayed error message:" 156 | msgstr "Remplacement des messages d'erreur affichés :" 157 | 158 | #: photobooth/gui/Qt5Gui/Frames.py:617 159 | msgid "Camera module:" 160 | msgstr "Module d'appareil photo :" 161 | 162 | #: photobooth/gui/Qt5Gui/Frames.py:681 photobooth/gui/Qt5Gui/Frames.py:684 163 | msgid "Select file" 164 | msgstr "Sélectionner le fichier" 165 | 166 | #: photobooth/gui/Qt5Gui/Frames.py:692 167 | msgid "Number of shots per picture:" 168 | msgstr "Nombre de capture par image :" 169 | 170 | #: photobooth/gui/Qt5Gui/Frames.py:693 171 | msgid "Size of assembled picture [px]:" 172 | msgstr "Taille de l'image assemblée [px] :" 173 | 174 | #: photobooth/gui/Qt5Gui/Frames.py:694 175 | msgid "Min. distance between shots [px]:" 176 | msgstr "Distance minim" 177 | 178 | #: photobooth/gui/Qt5Gui/Frames.py:695 179 | msgid "Omit last picture:" 180 | msgstr "Omettre la dernière image :" 181 | 182 | #: photobooth/gui/Qt5Gui/Frames.py:696 183 | msgid "Background image:" 184 | msgstr "Image d'arrière-plan :" 185 | 186 | #: photobooth/gui/Qt5Gui/Frames.py:717 photobooth/gui/Qt5Gui/Frames.py:721 187 | msgid "Select directory" 188 | msgstr "Sélectionner le dossier" 189 | 190 | #: photobooth/gui/Qt5Gui/Frames.py:729 191 | msgid "Output directory (strftime possible):" 192 | msgstr "Répertoire de sortie (strftime possible) :" 193 | 194 | #: photobooth/gui/Qt5Gui/Frames.py:730 195 | msgid "Basename of files (strftime possible):" 196 | msgstr "Nom de base des fichiers (strftime possible) :" 197 | 198 | #: photobooth/gui/Qt5Gui/Frames.py:731 199 | msgid "Keep single shots:" 200 | msgstr "Garder les images séparées :" 201 | 202 | #: photobooth/gui/Qt5Gui/Frames.py:781 203 | msgid "Enable GPIO:" 204 | msgstr "Activer les GPIO :" 205 | 206 | #: photobooth/gui/Qt5Gui/Frames.py:782 207 | msgid "Exit button pin (BCM numbering):" 208 | msgstr "Pin de bouton de sortie (numérotation BCM) :" 209 | 210 | #: photobooth/gui/Qt5Gui/Frames.py:783 211 | msgid "Trigger button pin (BCM numbering):" 212 | msgstr "Pin de bouton de déclenchement (numérotation BCM) :" 213 | 214 | #: photobooth/gui/Qt5Gui/Frames.py:784 215 | msgid "Idle lamp pin (BCM numbering):" 216 | msgstr "Pin pour lampe de veille (numérotation BCM) :" 217 | 218 | #: photobooth/gui/Qt5Gui/Frames.py:785 219 | msgid "RGB LED pins (BCM numbering):" 220 | msgstr "Pins pour LED RGB (numérotation BCM) :" 221 | 222 | #: photobooth/gui/Qt5Gui/Frames.py:826 223 | msgid "Enable printing:" 224 | msgstr "Permettre l'impression :" 225 | 226 | #: photobooth/gui/Qt5Gui/Frames.py:827 227 | msgid "Module:" 228 | msgstr "Module :" 229 | 230 | #: photobooth/gui/Qt5Gui/Frames.py:828 231 | msgid "Print to PDF (for debugging):" 232 | msgstr "Imprimer en PDF (à des fins de débogage)" 233 | 234 | #: photobooth/gui/Qt5Gui/Frames.py:829 235 | msgid "Ask for confirmation before printing:" 236 | msgstr "Demander une confirmation avant impression :" 237 | 238 | #: photobooth/gui/Qt5Gui/Frames.py:830 239 | msgid "Paper size [mm]:" 240 | msgstr "Taille du papier [mm] :" 241 | 242 | #: photobooth/gui/Qt5Gui/PyQt5Gui.py:164 243 | msgid "Starting the photobooth..." 244 | msgstr "Démarrage du photobooth ..." 245 | 246 | #: photobooth/gui/Qt5Gui/PyQt5Gui.py:214 247 | msgid "Processing picture..." 248 | msgstr "Traitement de l'image ..." 249 | 250 | #: photobooth/gui/Qt5Gui/PyQt5Gui.py:286 251 | msgid "Confirmation" 252 | msgstr "Confirmation" 253 | 254 | #: photobooth/gui/Qt5Gui/PyQt5Gui.py:287 255 | msgid "Quit Photobooth?" 256 | msgstr "Quitter Photobooth ?" 257 | 258 | -------------------------------------------------------------------------------- /photobooth/main.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | 4 | # Photobooth - a flexible photo booth software 5 | # Copyright (C) 2018 Balthasar Reuter 6 | # 7 | # This program is free software: you can redistribute it and/or modify 8 | # it under the terms of the GNU Affero General Public License as published by 9 | # the Free Software Foundation, either version 3 of the License, or 10 | # (at your option) any later version. 11 | # 12 | # This program is distributed in the hope that it will be useful, 13 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 14 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 15 | # GNU Affero General Public License for more details. 16 | # 17 | # You should have received a copy of the GNU Affero General Public License 18 | # along with this program. If not, see . 19 | 20 | # Provide installed photobooth version 21 | from pkg_resources import get_distribution, DistributionNotFound 22 | try: 23 | __version__ = get_distribution('photobooth').version 24 | except DistributionNotFound: 25 | __version__ = 'unknown' 26 | 27 | import argparse 28 | import gettext 29 | import logging 30 | import logging.handlers 31 | import multiprocessing as mp 32 | 33 | from . import camera, gui 34 | from .Config import Config 35 | from .gpio import Gpio 36 | from .util import lookup_and_import 37 | from .StateMachine import Context, ErrorEvent 38 | from .Threading import Communicator, Workers 39 | from .worker import Worker 40 | 41 | # Globally install gettext for I18N 42 | gettext.install('photobooth', 'photobooth/locale') 43 | 44 | 45 | class CameraProcess(mp.Process): 46 | 47 | def __init__(self, argv, config, comm): 48 | 49 | super().__init__() 50 | self.daemon = True 51 | 52 | self._cfg = config 53 | self._comm = comm 54 | 55 | def run(self): 56 | 57 | logging.debug('CameraProcess: Initializing...') 58 | 59 | CameraModule = lookup_and_import( 60 | camera.modules, self._cfg.get('Camera', 'module'), 'camera') 61 | cap = camera.Camera(self._cfg, self._comm, CameraModule) 62 | 63 | while True: 64 | try: 65 | logging.debug('CameraProcess: Running...') 66 | if cap.run(): 67 | break 68 | except Exception as e: 69 | logging.exception('CameraProcess: Exception "{}"'.format(e)) 70 | self._comm.send(Workers.MASTER, ErrorEvent('Camera', str(e))) 71 | 72 | logging.debug('CameraProcess: Exit') 73 | 74 | 75 | class GuiProcess(mp.Process): 76 | 77 | def __init__(self, argv, config, comm): 78 | 79 | super().__init__() 80 | 81 | self._argv = argv 82 | self._cfg = config 83 | self._comm = comm 84 | 85 | def run(self): 86 | 87 | logging.debug('GuiProcess: Initializing...') 88 | Gui = lookup_and_import(gui.modules, self._cfg.get('Gui', 'module'), 89 | 'gui') 90 | logging.debug('GuiProcess: Running...') 91 | retval = Gui(self._argv, self._cfg, self._comm).run() 92 | logging.debug('GuiProcess: Exit') 93 | return retval 94 | 95 | 96 | class WorkerProcess(mp.Process): 97 | 98 | def __init__(self, argv, config, comm): 99 | 100 | super().__init__() 101 | self.daemon = True 102 | 103 | self._cfg = config 104 | self._comm = comm 105 | 106 | def run(self): 107 | 108 | logging.debug('WorkerProcess: Initializing...') 109 | 110 | while True: 111 | try: 112 | logging.debug('WorkerProcess: Running...') 113 | if Worker(self._cfg, self._comm).run(): 114 | break 115 | except Exception as e: 116 | logging.exception('WorkerProcess: Exception "{}"'.format(e)) 117 | self._comm.send(Workers.MASTER, ErrorEvent('Worker', str(e))) 118 | 119 | logging.debug('WorkerProcess: Exit') 120 | 121 | 122 | class GpioProcess(mp.Process): 123 | 124 | def __init__(self, argv, config, comm): 125 | 126 | super().__init__() 127 | self.daemon = True 128 | 129 | self._cfg = config 130 | self._comm = comm 131 | 132 | def run(self): 133 | 134 | logging.debug('GpioProcess: Initializing...') 135 | 136 | while True: 137 | try: 138 | logging.debug('GpioProcess: Running...') 139 | if Gpio(self._cfg, self._comm).run(): 140 | break 141 | except Exception as e: 142 | logging.exception('GpioProcess: Exception "{}"'.format(e)) 143 | self._comm.send(Workers.MASTER, ErrorEvent('Gpio', str(e))) 144 | 145 | logging.debug('GpioProcess: Exit') 146 | 147 | 148 | def parseArgs(argv): 149 | 150 | # Add parameter for direct startup 151 | parser = argparse.ArgumentParser() 152 | parser.add_argument('--run', action='store_true', 153 | help='omit welcome screen and run photobooth') 154 | parser.add_argument('--debug', action='store_true', 155 | help='enable additional debug output') 156 | return parser.parse_known_args() 157 | 158 | 159 | def mainloop(comm, context): 160 | 161 | while True: 162 | try: 163 | for event in comm.iter(Workers.MASTER): 164 | exit_code = context.handleEvent(event) 165 | if exit_code in (0, 123): 166 | return exit_code 167 | except Exception as e: 168 | logging.exception('Main: Exception "{}"'.format(e)) 169 | comm.send(Workers.MASTER, ErrorEvent('Gpio', str(e))) 170 | 171 | 172 | def run(argv, is_run): 173 | 174 | logging.info('Photobooth version: %s', __version__) 175 | 176 | # Load configuration 177 | config = Config('photobooth.cfg') 178 | 179 | comm = Communicator() 180 | context = Context(comm, is_run) 181 | 182 | # Initialize processes: We use five processes here: 183 | # 1. Master that collects events and distributes state changes 184 | # 2. Camera handling 185 | # 3. GUI 186 | # 4. Postprocessing worker 187 | # 5. GPIO handler 188 | proc_classes = (CameraProcess, WorkerProcess, GuiProcess, GpioProcess) 189 | procs = [P(argv, config, comm) for P in proc_classes] 190 | 191 | for proc in procs: 192 | proc.start() 193 | 194 | # Enter main loop 195 | exit_code = mainloop(comm, context) 196 | 197 | # Wait for processes to finish 198 | for proc in procs: 199 | proc.join() 200 | 201 | logging.debug('All processes joined, returning code {}'. format(exit_code)) 202 | 203 | return exit_code 204 | 205 | 206 | def main(argv): 207 | 208 | # Parse command line arguments 209 | parsed_args, unparsed_args = parseArgs(argv) 210 | argv = argv[:1] + unparsed_args 211 | 212 | # Setup log level and format 213 | if parsed_args.debug: 214 | log_level = logging.DEBUG 215 | else: 216 | log_level = logging.INFO 217 | formatter = logging.Formatter( 218 | '%(asctime)s - %(name)s - %(levelname)s - %(message)s') 219 | 220 | # create console handler and set format 221 | ch = logging.StreamHandler() 222 | ch.setFormatter(formatter) 223 | 224 | # create file handler and set format 225 | fh = logging.handlers.TimedRotatingFileHandler('photobooth.log', when='d', 226 | interval=1, backupCount=10) 227 | fh.setFormatter(formatter) 228 | 229 | # Apply config 230 | logging.basicConfig(level=log_level, handlers=(ch, fh)) 231 | 232 | # Set of known status codes which trigger a restart of the application 233 | known_status_codes = { 234 | 999: 'Initializing photobooth', 235 | 123: 'Restarting photobooth and reloading config' 236 | } 237 | 238 | # Run the application until a status code not in above list is encountered 239 | status_code = 999 240 | 241 | while status_code in known_status_codes: 242 | logging.info(known_status_codes[status_code]) 243 | 244 | status_code = run(argv, parsed_args.run) 245 | 246 | logging.info('Exiting photobooth with status code %d', status_code) 247 | 248 | return status_code 249 | --------------------------------------------------------------------------------