decksmith 0.9.2__py3-none-any.whl → 0.9.3__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,6 +1,6 @@
1
- document.addEventListener('DOMContentLoaded', function() {
1
+ document.addEventListener('DOMContentLoaded', function () {
2
2
  // --- Initialization ---
3
-
3
+
4
4
  // Initialize Split.js
5
5
  Split(['#left-pane', '#right-pane'], {
6
6
  sizes: [50, 50],
@@ -48,7 +48,7 @@ document.addEventListener('DOMContentLoaded', function() {
48
48
  statusIcon: document.getElementById('status-icon'),
49
49
  statusLine: document.getElementById('status-line'),
50
50
  toastContainer: document.getElementById('toast-container'),
51
-
51
+
52
52
  // Project controls
53
53
  currentProjectPath: document.getElementById('current-project-path'),
54
54
  openProjectBtn: document.getElementById('open-project-btn'),
@@ -62,7 +62,7 @@ document.addEventListener('DOMContentLoaded', function() {
62
62
  projectNameInput: document.getElementById('project-name-input'),
63
63
  modalCancelBtn: document.getElementById('modal-cancel-btn'),
64
64
  modalConfirmBtn: document.getElementById('modal-confirm-btn'),
65
-
65
+
66
66
  // Welcome screen & Browse
67
67
  welcomeScreen: document.getElementById('welcome-screen'),
68
68
  welcomeOpenBtn: document.getElementById('welcome-open-btn'),
@@ -93,9 +93,30 @@ document.addEventListener('DOMContentLoaded', function() {
93
93
  modalMode: 'open', // 'open' or 'new'
94
94
  isPreviewing: false,
95
95
  pendingPreview: false,
96
- isProjectOpen: false
96
+ isProjectOpen: false,
97
+ hasSyntaxError: false
97
98
  };
98
99
 
100
+ // --- Annotation Handler ---
101
+ function checkAnnotations() {
102
+ // Wait slightly for Ace to update annotations
103
+ setTimeout(() => {
104
+ const annotations = yamlEditor.getSession().getAnnotations();
105
+ const errors = annotations.filter(a => a.type === 'error');
106
+
107
+ if (errors.length > 0) {
108
+ state.hasSyntaxError = true;
109
+ // Use the first error message
110
+ setStatus(`YAML Syntax Error: ${errors[0].text} (Line ${errors[0].row + 1})`, 'error');
111
+ } else {
112
+ if (state.hasSyntaxError) {
113
+ state.hasSyntaxError = false;
114
+ setStatus('Ready'); // Clear error status
115
+ }
116
+ }
117
+ }, 100);
118
+ }
119
+
99
120
  // --- UI Helpers ---
100
121
 
101
122
  function showShutdownScreen(reason) {
@@ -107,9 +128,9 @@ document.addEventListener('DOMContentLoaded', function() {
107
128
  const toast = document.createElement('div');
108
129
  toast.className = `toast ${type}`;
109
130
  toast.textContent = message;
110
-
131
+
111
132
  elements.toastContainer.appendChild(toast);
112
-
133
+
113
134
  requestAnimationFrame(() => {
114
135
  toast.classList.add('show');
115
136
  });
@@ -131,12 +152,15 @@ document.addEventListener('DOMContentLoaded', function() {
131
152
 
132
153
  function setStatus(message, type = null) {
133
154
  elements.statusText.textContent = message;
134
-
155
+
135
156
  // Reset status bar state
136
- elements.statusBar.classList.remove('processing', 'success');
157
+ elements.statusBar.classList.remove('processing', 'success', 'error');
137
158
  elements.statusSpinner.classList.add('hidden');
138
159
  elements.statusIcon.classList.remove('hidden');
139
160
 
161
+ // Reset icon to default
162
+ elements.statusIcon.className = 'fa-solid fa-circle-info fa-fw';
163
+
140
164
  if (type === 'processing') {
141
165
  elements.statusBar.classList.add('processing');
142
166
  elements.statusSpinner.classList.remove('hidden');
@@ -145,6 +169,11 @@ document.addEventListener('DOMContentLoaded', function() {
145
169
  } else if (type === 'success') {
146
170
  elements.statusBar.classList.add('success');
147
171
  updateStatusLine('success');
172
+ } else if (type === 'error') {
173
+ elements.statusBar.classList.add('error');
174
+ updateStatusLine('error');
175
+ // Change icon to danger
176
+ elements.statusIcon.className = 'fa-solid fa-triangle-exclamation fa-fw';
148
177
  } else {
149
178
  if (type) updateStatusLine(type);
150
179
  }
@@ -194,7 +223,7 @@ document.addEventListener('DOMContentLoaded', function() {
194
223
  option.textContent = `${index + 1}: ${label}`;
195
224
  elements.cardSelector.appendChild(option);
196
225
  });
197
-
226
+
198
227
  if (state.currentCardIndex >= 0 && state.currentCardIndex < cards.length) {
199
228
  elements.cardSelector.value = state.currentCardIndex;
200
229
  } else if (cards.length > 0) {
@@ -222,7 +251,7 @@ document.addEventListener('DOMContentLoaded', function() {
222
251
  state.currentCardIndex = index;
223
252
  state.isPreviewing = true;
224
253
  state.pendingPreview = false;
225
-
254
+
226
255
  elements.loadingIndicator.classList.remove('hidden');
227
256
  elements.placeholderText.classList.add('hidden');
228
257
  setStatus('Generating preview...', 'loading');
@@ -237,33 +266,33 @@ document.addEventListener('DOMContentLoaded', function() {
237
266
  headers: { 'Content-Type': 'application/json' },
238
267
  body: JSON.stringify(payload)
239
268
  })
240
- .then(response => {
241
- if (!response.ok) {
242
- return response.json().then(err => { throw new Error(err.error || 'Preview failed'); });
243
- }
244
- return response.blob();
245
- })
246
- .then(blob => {
247
- const url = URL.createObjectURL(blob);
248
- elements.previewImage.src = url;
249
- elements.previewImage.onload = () => {
269
+ .then(response => {
270
+ if (!response.ok) {
271
+ return response.json().then(err => { throw new Error(err.error || 'Preview failed'); });
272
+ }
273
+ return response.blob();
274
+ })
275
+ .then(blob => {
276
+ const url = URL.createObjectURL(blob);
277
+ elements.previewImage.src = url;
278
+ elements.previewImage.onload = () => {
279
+ elements.loadingIndicator.classList.add('hidden');
280
+ elements.previewImage.classList.remove('hidden');
281
+ setStatus('Preview updated');
282
+ updateStatusLine('success');
283
+ };
284
+ })
285
+ .catch(err => {
250
286
  elements.loadingIndicator.classList.add('hidden');
251
- elements.previewImage.classList.remove('hidden');
252
- setStatus('Preview updated');
253
- updateStatusLine('success');
254
- };
255
- })
256
- .catch(err => {
257
- elements.loadingIndicator.classList.add('hidden');
258
- setStatus(`Error: ${err.message}`, 'error');
259
- console.error(err);
260
- })
261
- .finally(() => {
262
- state.isPreviewing = false;
263
- if (state.pendingPreview) {
264
- updatePreview();
265
- }
266
- });
287
+ setStatus(`Error: ${err.message}`, 'error');
288
+ console.error(err);
289
+ })
290
+ .finally(() => {
291
+ state.isPreviewing = false;
292
+ if (state.pendingPreview) {
293
+ updatePreview();
294
+ }
295
+ });
267
296
  }
268
297
 
269
298
  function autoSave() {
@@ -272,7 +301,7 @@ document.addEventListener('DOMContentLoaded', function() {
272
301
  if (!elements.statusText.textContent.includes('preview')) {
273
302
  setStatus('Saving...');
274
303
  }
275
-
304
+
276
305
  const payload = {
277
306
  yaml: yamlEditor.getValue(),
278
307
  csv: csvEditor.getValue()
@@ -283,17 +312,17 @@ document.addEventListener('DOMContentLoaded', function() {
283
312
  headers: { 'Content-Type': 'application/json' },
284
313
  body: JSON.stringify(payload)
285
314
  })
286
- .then(response => response.json())
287
- .then(data => {
288
- if (!elements.statusText.textContent.includes('preview')) {
289
- setStatus('Saved');
290
- }
291
- loadCards();
292
- })
293
- .catch(err => {
294
- setStatus('Error saving');
295
- console.error(err);
296
- });
315
+ .then(response => response.json())
316
+ .then(data => {
317
+ // if (!elements.statusText.textContent.includes('preview')) {
318
+ // setStatus('Saved');
319
+ // }
320
+ loadCards();
321
+ })
322
+ .catch(err => {
323
+ setStatus('Error saving');
324
+ console.error(err);
325
+ });
297
326
  }
298
327
 
299
328
  function buildDeck() {
@@ -302,21 +331,24 @@ document.addEventListener('DOMContentLoaded', function() {
302
331
  yaml: yamlEditor.getValue(),
303
332
  csv: csvEditor.getValue()
304
333
  };
305
-
306
- fetch('/api/build', {
334
+
335
+ return fetch('/api/build', {
307
336
  method: 'POST',
308
337
  headers: { 'Content-Type': 'application/json' },
309
338
  body: JSON.stringify(payload)
310
339
  })
311
- .then(response => response.json())
312
- .then(data => {
313
- if (data.error) throw new Error(data.error);
314
- setStatus(data.message, 'success');
315
- })
316
- .catch(err => {
317
- setStatus('Build failed', 'error');
318
- showNotification('Build failed: ' + err.message, 'error');
319
- });
340
+ .then(response => response.json())
341
+ .then(data => {
342
+ if (data.error) throw new Error(data.error);
343
+ setStatus(data.message, 'success');
344
+ showNotification(data.message, 'success');
345
+ return data;
346
+ })
347
+ .catch(err => {
348
+ setStatus('Build failed: ' + err.message, 'error');
349
+ showNotification('Build failed: ' + err.message, 'error');
350
+ throw err;
351
+ });
320
352
  }
321
353
 
322
354
  function showExportModal() {
@@ -339,7 +371,16 @@ document.addEventListener('DOMContentLoaded', function() {
339
371
  };
340
372
 
341
373
  hideExportModal();
342
- exportPdf(params);
374
+
375
+ // Chain build then export
376
+ buildDeck()
377
+ .then(() => {
378
+ exportPdf(params);
379
+ })
380
+ .catch(() => {
381
+ // Build failed, error already handled by buildDeck UI updates
382
+ console.log('Export cancelled due to build failure');
383
+ });
343
384
  }
344
385
 
345
386
  function exportPdf(params) {
@@ -349,15 +390,16 @@ document.addEventListener('DOMContentLoaded', function() {
349
390
  headers: { 'Content-Type': 'application/json' },
350
391
  body: JSON.stringify(params)
351
392
  })
352
- .then(response => response.json())
353
- .then(data => {
354
- if (data.error) throw new Error(data.error);
355
- setStatus(data.message, 'success');
356
- })
357
- .catch(err => {
358
- setStatus('Export failed', 'error');
359
- showNotification('Export failed: ' + err.message, 'error');
360
- });
393
+ .then(response => response.json())
394
+ .then(data => {
395
+ if (data.error) throw new Error(data.error);
396
+ setStatus(data.message, 'success');
397
+ showNotification(data.message, 'success');
398
+ })
399
+ .catch(err => {
400
+ setStatus('Export failed: ' + err.message, 'error');
401
+ showNotification('Export failed: ' + err.message, 'error');
402
+ });
361
403
  }
362
404
 
363
405
  // --- Project Management ---
@@ -367,6 +409,8 @@ document.addEventListener('DOMContentLoaded', function() {
367
409
  elements.modalTitle.textContent = mode === 'open' ? 'Open Project' : 'New Project';
368
410
  elements.projectPathInput.value = '';
369
411
  elements.projectNameInput.value = '';
412
+ elements.projectPathInput.classList.remove('input-error');
413
+ elements.projectNameInput.classList.remove('input-error');
370
414
  elements.pathModal.classList.remove('hidden');
371
415
 
372
416
  if (mode === 'new') {
@@ -414,24 +458,23 @@ document.addEventListener('DOMContentLoaded', function() {
414
458
  headers: { 'Content-Type': 'application/json' },
415
459
  body: JSON.stringify({ path: path })
416
460
  })
417
- .then(response => response.json())
418
- .then(data => {
419
- if (data.error) throw new Error(data.error);
420
-
421
- state.isProjectOpen = true;
422
- elements.currentProjectPath.textContent = data.path;
423
- elements.currentProjectPath.title = data.path;
424
-
425
- // showNotification('Project opened', 'success');
426
- setStatus('Ready');
427
-
428
- loadInitialData();
429
- elements.welcomeScreen.classList.add('hidden');
430
- })
431
- .catch(err => {
432
- showNotification(err.message, 'error');
433
- setStatus('Error', 'error');
434
- });
461
+ .then(response => response.json())
462
+ .then(data => {
463
+ if (data.error) throw new Error(data.error);
464
+
465
+ state.isProjectOpen = true;
466
+ elements.currentProjectPath.textContent = data.path;
467
+ elements.currentProjectPath.title = data.path;
468
+
469
+ // showNotification('Project opened', 'success');
470
+ setStatus('Ready');
471
+
472
+ loadInitialData();
473
+ elements.welcomeScreen.classList.add('hidden');
474
+ })
475
+ .catch(err => {
476
+ setStatus('Error: ' + err.message, 'error');
477
+ });
435
478
  }
436
479
 
437
480
  function handleDirectOpen() {
@@ -445,17 +488,28 @@ document.addEventListener('DOMContentLoaded', function() {
445
488
  .catch(err => console.error('Error browsing:', err));
446
489
  }
447
490
 
491
+ // Clear error on input
492
+ elements.projectPathInput.addEventListener('input', () => {
493
+ elements.projectPathInput.classList.remove('input-error');
494
+ });
495
+
496
+ elements.projectNameInput.addEventListener('input', () => {
497
+ elements.projectNameInput.classList.remove('input-error');
498
+ });
499
+
448
500
  function handleProjectAction() {
449
501
  let path = elements.projectPathInput.value.trim();
450
502
  if (!path) {
451
- showNotification('Please enter a path', 'error');
503
+ elements.projectPathInput.classList.add('input-error');
504
+ // showNotification('Please enter a path', 'error');
452
505
  return;
453
506
  }
454
507
 
455
508
  if (state.modalMode === 'new') {
456
509
  const name = elements.projectNameInput.value.trim();
457
510
  if (!name) {
458
- showNotification('Please enter a project name', 'error');
511
+ elements.projectNameInput.classList.add('input-error');
512
+ // showNotification('Please enter a project name', 'error');
459
513
  return;
460
514
  }
461
515
  const separator = path.includes('\\') ? '\\' : '/';
@@ -463,7 +517,7 @@ document.addEventListener('DOMContentLoaded', function() {
463
517
  }
464
518
 
465
519
  const endpoint = state.modalMode === 'open' ? '/api/project/select' : '/api/project/create';
466
-
520
+
467
521
  setStatus(state.modalMode === 'open' ? 'Opening project...' : 'Creating project...');
468
522
 
469
523
  fetch(endpoint, {
@@ -471,26 +525,25 @@ document.addEventListener('DOMContentLoaded', function() {
471
525
  headers: { 'Content-Type': 'application/json' },
472
526
  body: JSON.stringify({ path: path })
473
527
  })
474
- .then(response => response.json())
475
- .then(data => {
476
- if (data.error) throw new Error(data.error);
477
-
478
- hideModal();
479
- state.isProjectOpen = true;
480
- elements.currentProjectPath.textContent = data.path;
481
- elements.currentProjectPath.title = data.path;
482
-
483
- // showNotification(state.modalMode === 'open' ? 'Project opened' : 'Project created', 'success');
484
- setStatus('Ready');
485
-
486
- // Reload data
487
- loadInitialData();
488
- elements.welcomeScreen.classList.add('hidden');
489
- })
490
- .catch(err => {
491
- showNotification(err.message, 'error');
492
- setStatus('Error', 'error');
493
- });
528
+ .then(response => response.json())
529
+ .then(data => {
530
+ if (data.error) throw new Error(data.error);
531
+
532
+ hideModal();
533
+ state.isProjectOpen = true;
534
+ elements.currentProjectPath.textContent = data.path;
535
+ elements.currentProjectPath.title = data.path;
536
+
537
+ // showNotification(state.modalMode === 'open' ? 'Project opened' : 'Project created', 'success');
538
+ setStatus('Ready');
539
+
540
+ // Reload data
541
+ loadInitialData();
542
+ elements.welcomeScreen.classList.add('hidden');
543
+ })
544
+ .catch(err => {
545
+ setStatus('Error: ' + err.message, 'error');
546
+ });
494
547
  }
495
548
 
496
549
  function closeProject() {
@@ -500,16 +553,16 @@ document.addEventListener('DOMContentLoaded', function() {
500
553
  state.isProjectOpen = false;
501
554
  elements.currentProjectPath.textContent = 'No project selected';
502
555
  elements.currentProjectPath.title = '';
503
-
556
+
504
557
  // Clear editors
505
558
  yamlEditor.setValue('', -1);
506
559
  csvEditor.setValue('', -1);
507
560
  state.isDirty = false; // Reset dirty flag after clearing editors
508
-
561
+
509
562
  elements.cardSelector.innerHTML = '<option value="-1">Select a card...</option>';
510
563
  elements.previewImage.classList.add('hidden');
511
564
  elements.placeholderText.classList.remove('hidden');
512
-
565
+
513
566
  elements.welcomeScreen.classList.remove('hidden');
514
567
  // showNotification('Project closed', 'info');
515
568
  })
@@ -542,6 +595,7 @@ document.addEventListener('DOMContentLoaded', function() {
542
595
  };
543
596
 
544
597
  yamlEditor.session.on('change', markDirty);
598
+ yamlEditor.session.on('changeAnnotation', checkAnnotations);
545
599
  csvEditor.session.on('change', markDirty);
546
600
 
547
601
  // Auto-save loop
@@ -569,7 +623,7 @@ document.addEventListener('DOMContentLoaded', function() {
569
623
  evtSource.onmessage = (e) => {
570
624
  // Ignore keepalive
571
625
  if (e.data === ': keepalive') return;
572
-
626
+
573
627
  try {
574
628
  const data = JSON.parse(e.data);
575
629
  if (data.type === 'shutdown') {
decksmith/image_ops.py CHANGED
@@ -119,3 +119,14 @@ class ImageOps:
119
119
  if direction == "vertical":
120
120
  return img.transpose(Image.FLIP_TOP_BOTTOM)
121
121
  return img
122
+
123
+ @staticmethod
124
+ def _filter_opacity(img: Image.Image, opacity: int) -> Image.Image:
125
+ opacity = max(opacity, 0)
126
+ opacity = min(opacity, 100)
127
+
128
+ img = img.convert("RGBA")
129
+ alpha = img.split()[3]
130
+ alpha = alpha.point(lambda p: int(p * opacity / 100))
131
+ img.putalpha(alpha)
132
+ return img
decksmith/main.py CHANGED
@@ -80,8 +80,7 @@ def build(ctx, output, spec, data):
80
80
  # pylint: disable=W0718
81
81
  except Exception as exc:
82
82
  logger.error("(x) Error building deck '%s' from spec '%s':", data, spec)
83
- logger.error(" %s", exc)
84
- logger.debug(traceback.format_exc())
83
+ logger.error(" %s\n%s", exc, traceback.format_exc())
85
84
  ctx.exit(1)
86
85
 
87
86
  logger.info("(✔) Deck built successfully.")
@@ -114,7 +113,8 @@ def build(ctx, output, spec, data):
114
113
  default=[2, 2],
115
114
  help="The horizontal and vertical page margins in millimeters.",
116
115
  )
117
- def export(image_folder, output, page_size, width, height, gap, margins):
116
+ @click.pass_context
117
+ def export(ctx, image_folder, output, page_size, width, height, gap, margins):
118
118
  """Exports images from a folder to a PDF file."""
119
119
  try:
120
120
  image_folder_path = Path(image_folder)
@@ -133,12 +133,13 @@ def export(image_folder, output, page_size, width, height, gap, margins):
133
133
  exporter.export()
134
134
  logger.info("(✔) Successfully exported PDF to %s", output)
135
135
  except FileNotFoundError as exc:
136
- logger.error("(x) %s", exc)
136
+ click.echo(f"(x) {exc}")
137
+ ctx.exit(1)
137
138
  # pylint: disable=W0718
138
139
  except Exception as exc:
139
140
  logger.error("(x) Error exporting images to '%s':", output)
140
- logger.error(" %s", exc)
141
- logger.debug(traceback.format_exc())
141
+ logger.error(" %s\n%s", exc, traceback.format_exc())
142
+ ctx.exit(1)
142
143
 
143
144
 
144
145
  if __name__ == "__main__":
decksmith/project.py CHANGED
@@ -30,16 +30,12 @@ class ProjectManager:
30
30
  def create_project(self, path: Path):
31
31
  """
32
32
  Creates a new project at the specified path.
33
+
33
34
  Args:
34
- path (Path): The path to create the project in.
35
+ path (Path): The path where the project directory will be created.
35
36
  """
36
37
  path.mkdir(parents=True, exist_ok=True)
37
38
 
38
- # Copy templates
39
- # Assuming templates are in decksmith/templates relative to this file
40
- # But actually they are in decksmith/templates relative to the package root
41
- # Let's use importlib.resources or relative path from __file__
42
- # Since we are in decksmith/project.py, templates are in ../templates
43
39
  template_dir = Path(__file__).parent / "templates"
44
40
 
45
41
  if not (path / "deck.yaml").exists():
@@ -50,52 +46,52 @@ class ProjectManager:
50
46
 
51
47
  self.working_dir = path
52
48
 
53
- def load_files(self) -> Dict[str, str]:
49
+ def _load_file_or_template(self, filename: str) -> str:
54
50
  """
55
- Loads the deck.yaml and deck.csv files from the current project.
51
+ Helper to load a file from the working directory, or fall back to a template.
52
+
53
+ Args:
54
+ filename (str): The name of the file (e.g., "deck.yaml").
55
+
56
56
  Returns:
57
- Dict[str, str]: A dictionary containing the content of the files.
57
+ str: The content of the file or template.
58
58
  """
59
59
  if self.working_dir is None:
60
- return {"yaml": "", "csv": ""}
60
+ return ""
61
61
 
62
- yaml_path = self.working_dir / "deck.yaml"
63
- csv_path = self.working_dir / "deck.csv"
62
+ file_path = self.working_dir / filename
63
+ template_path = Path(__file__).parent / "templates" / filename
64
64
 
65
- template_dir = Path(__file__).parent / "templates"
66
- yaml_template = template_dir / "deck.yaml"
67
- csv_template = template_dir / "deck.csv"
68
-
69
- data = {}
70
-
71
- # Load YAML
72
- if yaml_path.exists() and yaml_path.stat().st_size > 0:
73
- with open(yaml_path, "r", encoding="utf-8") as yaml_file:
74
- data["yaml"] = yaml_file.read()
75
- elif yaml_template.exists():
76
- with open(yaml_template, "r", encoding="utf-8") as yaml_template_file:
77
- data["yaml"] = yaml_template_file.read()
78
- else:
79
- data["yaml"] = ""
80
-
81
- # Load CSV
82
- if csv_path.exists() and csv_path.stat().st_size > 0:
83
- with open(csv_path, "r", encoding="utf-8") as csv_file:
84
- data["csv"] = csv_file.read()
85
- elif csv_template.exists():
86
- with open(csv_template, "r", encoding="utf-8") as csv_template_file:
87
- data["csv"] = csv_template_file.read()
88
- else:
89
- data["csv"] = ""
90
-
91
- return data
65
+ if file_path.exists() and file_path.stat().st_size > 0:
66
+ with open(file_path, "r", encoding="utf-8") as file_object:
67
+ return file_object.read()
68
+ elif template_path.exists():
69
+ with open(template_path, "r", encoding="utf-8") as file_object:
70
+ return file_object.read()
71
+ return ""
72
+
73
+ def load_files(self) -> Dict[str, str]:
74
+ """
75
+ Loads the deck.yaml and deck.csv files from the current project.
76
+
77
+ Returns:
78
+ Dict[str, str]: A dictionary with keys "yaml" and "csv" containing file contents.
79
+ """
80
+ return {
81
+ "yaml": self._load_file_or_template("deck.yaml"),
82
+ "csv": self._load_file_or_template("deck.csv"),
83
+ }
92
84
 
93
85
  def save_files(self, yaml_content: Optional[str], csv_content: Optional[str]):
94
86
  """
95
87
  Saves the deck.yaml and deck.csv files to the current project.
88
+
96
89
  Args:
97
90
  yaml_content (Optional[str]): The content of the deck.yaml file.
98
91
  csv_content (Optional[str]): The content of the deck.csv file.
92
+
93
+ Raises:
94
+ ValueError: If no project is currently selected (working_dir is None).
99
95
  """
100
96
  if self.working_dir is None:
101
97
  raise ValueError("No project selected")
@@ -9,7 +9,6 @@ from typing import Any, Dict, Optional
9
9
  from PIL import Image
10
10
 
11
11
  from decksmith.image_ops import ImageOps
12
- from decksmith.logger import logger
13
12
  from decksmith.utils import apply_anchor
14
13
 
15
14
 
@@ -46,11 +45,10 @@ class ImageRenderer:
46
45
  if potential_path.exists():
47
46
  path = potential_path
48
47
 
49
- try:
50
- img = Image.open(path)
51
- except FileNotFoundError:
52
- logger.error("Image not found: %s", path)
53
- return
48
+ if not path.exists():
49
+ raise FileNotFoundError(f"Image not found: {path}")
50
+
51
+ img = Image.open(path)
54
52
 
55
53
  img = ImageOps.apply_filters(img, element.get("filters", {}))
56
54