···
13
-
from PIL import Image
logger = logging.getLogger('camera_server')
···
PHOTO_DIR = "/home/ink/photos"
31
-
PHOTO_RESOLUTION = (2592, 1944)
30
+
PHOTO_RESOLUTION = (1280, 960)
···
body {{ font-family: Arial; max-width: 800px; margin: 0 auto; padding: 20px; }}
h1 {{ text-align: center; }}
.gallery {{ display: flex; flex-wrap: wrap; gap: 10px; justify-content: center; }}
64
-
.photo {{ border: 1px solid #ddd; padding: 5px; animation: fadeIn 0.1s; flex: 0 1 200px; }}
65
-
.photo img {{ width: 100%; height: auto; }}
63
+
.photo {{ border: 1px solid #ddd; padding: 5px; animation: fadeIn 0.1s; flex: 0 1 200px; position: relative; }}
64
+
.photo img {{ width: 100%; height: auto; transition: opacity 0.3s; }}
65
+
.photo .colored-img {{ position: absolute; top: 5px; left: 5px; opacity: 0; pointer-events: none; }}
66
+
.photo:hover .dithered-img {{ opacity: 0; }}
67
+
.photo:hover .colored-img {{ opacity: 1; }}
.photo .actions {{ text-align: center; margin-top: 5px; }}
.photo .actions a {{ margin: 0 5px; }}
@keyframes fadeIn {{ from {{ opacity: 0; }} to {{ opacity: 1; }} }}
···
108
-
const photoDiv = document.createElement('div');
109
-
photoDiv.className = 'photo';
110
-
photoDiv.id = `photo-${{filename}}`;
110
+
const originalFilename = filename.replace('dithered_', '');
111
+
const isDithered = filename.startsWith('dithered_');
112
-
photoDiv.innerHTML = `
113
-
<img src="/${{filename}}" alt="${{timestamp}}">
114
-
<div class="actions">
115
-
<a href="/${{filename}}" download>Download</a>
116
-
<a href="#" onclick="deletePhoto('${{filename}}'); return false;">Delete</a>
114
+
const photoDiv = document.createElement('div');
115
+
photoDiv.className = 'photo';
116
+
photoDiv.id = `photo-${{filename}}`;
118
+
photoDiv.innerHTML = `
119
+
<img class="dithered-img" src="/${{filename}}" alt="${{timestamp}}">
120
+
<img class="colored-img" src="/${{originalFilename}}" alt="${{timestamp}}">
121
+
<div class="actions">
122
+
<a href="/${{originalFilename}}" download>Download Color</a>
123
+
<a href="/${{filename}}" download>Download Dithered</a>
124
+
<a href="#" onclick="deletePhoto('${{filename}}', '${{originalFilename}}'); return false;">Delete</a>
120
-
gallery.insertBefore(photoDiv, gallery.firstChild);
128
+
gallery.insertBefore(photoDiv, gallery.firstChild);
function removePhoto(filename) {{
···
139
-
function deletePhoto(filename) {{
148
+
function deletePhoto(ditheredFilename, originalFilename) {{
if (confirm('Are you sure you want to delete this photo?')) {{
141
-
fetch('/delete/' + filename, {{
150
+
fetch('/delete/' + ditheredFilename, {{
145
-
removePhoto(filename);
154
+
return fetch('/delete/' + originalFilename, {{ method: 'POST' }});
156
+
}}).then(response => {{
158
+
removePhoto(ditheredFilename);
···
files = sorted(os.listdir(Config.PHOTO_DIR), reverse=True)
179
-
if filename.lower().endswith(('.jpg', '.jpeg', '.png')):
180
-
timestamp = filename.replace('photo_', '').replace('.jpg', '')
192
+
if filename.lower().endswith(('.jpg', '.jpeg', '.png')) and filename.startswith('dithered_'):
193
+
originalFilename = filename.replace('dithered_', 'photo_')
194
+
timestamp = filename.replace('dithered_', '').replace('.jpg', '')
<div class="photo" id="photo-{filename}">
183
-
<img src="/{filename}" alt="{timestamp}">
197
+
<img class="dithered-img" src="/{filename}" alt="{timestamp}">
198
+
<img class="colored-img" src="/{originalFilename}" alt="{timestamp}">
185
-
<a href="/{filename}" download>Download</a>
186
-
<a href="#" onclick="deletePhoto('{filename}'); return false;">Delete</a>
200
+
<a href="/{originalFilename}" download>Download Color</a>
201
+
<a href="/{filename}" download>Download Dithered</a>
202
+
<a href="#" onclick="deletePhoto('{filename}', '{originalFilename}'); return false;">Delete</a>
···
timestamp = datetime.now().strftime("%Y-%m-%d_%H-%M-%S")
filename = f"photo_{timestamp}.jpg"
283
+
dithered_filename = f"dithered_{timestamp}.jpg"
filepath = os.path.join(Config.PHOTO_DIR, filename)
285
+
dithered_filepath = os.path.join(Config.PHOTO_DIR, dithered_filename)
logger.info(f"Taking photo: {filepath}")
picam2.capture_file(filepath)
logger.info("Photo taken successfully")
273
-
# Rotate the image using PIL
274
-
with Image.open(filepath) as img:
275
-
rotated_img = img.rotate(Config.ROTATION, expand=True)
276
-
rotated_img.save(filepath)
277
-
logger.info("Photo rotated successfully")
291
+
# Rotate the image using ImageMagick
292
+
os.system(f"magick {filepath} -rotate {Config.ROTATION} {filepath}")
293
+
logger.info("Photo rotated successfully")
295
+
# Create dithered version using ImageMagick
296
+
os.system(f"magick {filepath} -dither FloydSteinberg -define dither:diffusion-amount=100% -remap eink-4gray.png {dithered_filepath}")
297
+
logger.info("Dithered version created successfully")
279
-
# Notify websocket clients about new photo
299
+
# Notify websocket clients about both photos
asyncio.run(notify_clients('new_photo', {
281
-
'filename': filename,
301
+
'filename': dithered_filename,
logger.error(f"IO Error while taking photo: {str(e)}")