libreflow.extensions.sk.export-psd-layers 1.0.0__tar.gz → 1.1.0__tar.gz

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.
Files changed (19) hide show
  1. libreflow_extensions_sk_export_psd_layers-1.1.0/CHANGELOG.md +54 -0
  2. {libreflow_extensions_sk_export_psd_layers-1.0.0/src/libreflow.extensions.sk.export_psd_layers.egg-info → libreflow_extensions_sk_export_psd_layers-1.1.0}/PKG-INFO +31 -1
  3. {libreflow_extensions_sk_export_psd_layers-1.0.0 → libreflow_extensions_sk_export_psd_layers-1.1.0}/setup.py +6 -11
  4. libreflow_extensions_sk_export_psd_layers-1.1.0/src/libreflow/extensions/sk/export_psd_layers/__init__.py +598 -0
  5. {libreflow_extensions_sk_export_psd_layers-1.0.0 → libreflow_extensions_sk_export_psd_layers-1.1.0}/src/libreflow/extensions/sk/export_psd_layers/_version.py +3 -3
  6. {libreflow_extensions_sk_export_psd_layers-1.0.0 → libreflow_extensions_sk_export_psd_layers-1.1.0/src/libreflow.extensions.sk.export_psd_layers.egg-info}/PKG-INFO +31 -1
  7. {libreflow_extensions_sk_export_psd_layers-1.0.0 → libreflow_extensions_sk_export_psd_layers-1.1.0}/src/libreflow.extensions.sk.export_psd_layers.egg-info/SOURCES.txt +1 -0
  8. libreflow_extensions_sk_export_psd_layers-1.1.0/src/libreflow.extensions.sk.export_psd_layers.egg-info/requires.txt +1 -0
  9. libreflow_extensions_sk_export_psd_layers-1.0.0/CHANGELOG.md +0 -26
  10. libreflow_extensions_sk_export_psd_layers-1.0.0/src/libreflow/extensions/sk/export_psd_layers/__init__.py +0 -125
  11. {libreflow_extensions_sk_export_psd_layers-1.0.0 → libreflow_extensions_sk_export_psd_layers-1.1.0}/MANIFEST.in +0 -0
  12. {libreflow_extensions_sk_export_psd_layers-1.0.0 → libreflow_extensions_sk_export_psd_layers-1.1.0}/README.md +0 -0
  13. {libreflow_extensions_sk_export_psd_layers-1.0.0 → libreflow_extensions_sk_export_psd_layers-1.1.0}/setup.cfg +0 -0
  14. {libreflow_extensions_sk_export_psd_layers-1.0.0 → libreflow_extensions_sk_export_psd_layers-1.1.0}/src/libreflow/__init__.py +0 -0
  15. {libreflow_extensions_sk_export_psd_layers-1.0.0 → libreflow_extensions_sk_export_psd_layers-1.1.0}/src/libreflow/extensions/__init__.py +0 -0
  16. {libreflow_extensions_sk_export_psd_layers-1.0.0 → libreflow_extensions_sk_export_psd_layers-1.1.0}/src/libreflow/extensions/sk/__init__.py +0 -0
  17. {libreflow_extensions_sk_export_psd_layers-1.0.0 → libreflow_extensions_sk_export_psd_layers-1.1.0}/src/libreflow.extensions.sk.export_psd_layers.egg-info/dependency_links.txt +0 -0
  18. {libreflow_extensions_sk_export_psd_layers-1.0.0 → libreflow_extensions_sk_export_psd_layers-1.1.0}/src/libreflow.extensions.sk.export_psd_layers.egg-info/top_level.txt +0 -0
  19. {libreflow_extensions_sk_export_psd_layers-1.0.0 → libreflow_extensions_sk_export_psd_layers-1.1.0}/versioneer.py +0 -0
@@ -0,0 +1,54 @@
1
+ # Changelog
2
+
3
+ All notable changes to this project will be documented in this file.
4
+
5
+ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
6
+ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html)[^1].
7
+
8
+ <!---
9
+ Types of changes
10
+
11
+ - Added for new features.
12
+ - Changed for changes in existing functionality.
13
+ - Deprecated for soon-to-be removed features.
14
+ - Removed for now removed features.
15
+ - Fixed for any bug fixes.
16
+ - Security in case of vulnerabilities.
17
+
18
+ -->
19
+
20
+ ## [Unreleased]
21
+
22
+ ## [1.1.0] - 2025-04-24
23
+
24
+ ### Added
25
+
26
+ * An action to export a psd for preview
27
+ * Use the `psd-tools` python module
28
+ * Three PNGs, one in full format, one with no characters and one without a safety margin
29
+ * These files are not tracked in Libreflow, they remain local
30
+ * A specific upload to kitsu action is available for this use case
31
+ * An action to publish and export at the same time
32
+ * An action to export layers in batch
33
+ * Available at a film hierarchy level
34
+
35
+ ### Changed
36
+
37
+ * Export layers action now uses `psd-tools` python module instead of a Adobe extend script
38
+ * Process can takes longer, but Photoshop is no longer required
39
+
40
+ ### Fixed
41
+
42
+ * Hide export layers action when file has no published revisions
43
+
44
+ ## [1.0.1] - 2025-04-04
45
+
46
+ ### Fixed
47
+
48
+ * including .jsx files in setup.py
49
+
50
+ ## [1.0.0] - 2025-03-27
51
+
52
+ ### Added
53
+
54
+ * Extension to export the layers of a Photoshop project as png images
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: libreflow.extensions.sk.export_psd_layers
3
- Version: 1.0.0
3
+ Version: 1.1.0
4
4
  Home-page: https://gitlab.com/lfs.coop/libreflow/libreflow_launcher
5
5
  Author: Thomas Thiebaut
6
6
  Author-email: autor@les-fees-speciales.coop
@@ -12,6 +12,7 @@ Classifier: License :: OSI Approved :: GNU Lesser General Public License v3 or l
12
12
  Classifier: Operating System :: OS Independent
13
13
  Requires-Python: >=3.7
14
14
  Description-Content-Type: text/markdown
15
+ Requires-Dist: psd-tools
15
16
  Dynamic: author
16
17
  Dynamic: author-email
17
18
  Dynamic: classifier
@@ -20,6 +21,7 @@ Dynamic: description-content-type
20
21
  Dynamic: home-page
21
22
  Dynamic: keywords
22
23
  Dynamic: license
24
+ Dynamic: requires-dist
23
25
  Dynamic: requires-python
24
26
 
25
27
  # Sk Export Psd Layers
@@ -48,6 +50,34 @@ Types of changes
48
50
 
49
51
  ## [Unreleased]
50
52
 
53
+ ## [1.1.0] - 2025-04-24
54
+
55
+ ### Added
56
+
57
+ * An action to export a psd for preview
58
+ * Use the `psd-tools` python module
59
+ * Three PNGs, one in full format, one with no characters and one without a safety margin
60
+ * These files are not tracked in Libreflow, they remain local
61
+ * A specific upload to kitsu action is available for this use case
62
+ * An action to publish and export at the same time
63
+ * An action to export layers in batch
64
+ * Available at a film hierarchy level
65
+
66
+ ### Changed
67
+
68
+ * Export layers action now uses `psd-tools` python module instead of a Adobe extend script
69
+ * Process can takes longer, but Photoshop is no longer required
70
+
71
+ ### Fixed
72
+
73
+ * Hide export layers action when file has no published revisions
74
+
75
+ ## [1.0.1] - 2025-04-04
76
+
77
+ ### Fixed
78
+
79
+ * including .jsx files in setup.py
80
+
51
81
  ## [1.0.0] - 2025-03-27
52
82
 
53
83
  ### Added
@@ -2,19 +2,18 @@ import setuptools
2
2
  import versioneer
3
3
  import os
4
4
 
5
- readme = os.path.normpath(os.path.join(__file__, '..', 'README.md'))
5
+ readme = os.path.normpath(os.path.join(__file__, "..", "README.md"))
6
6
  with open(readme, "r", encoding="utf-8") as fh:
7
7
  long_description = fh.read()
8
8
 
9
- long_description += '\n\n'
9
+ long_description += "\n\n"
10
10
 
11
- changelog = os.path.normpath(os.path.join(__file__, '..', 'CHANGELOG.md'))
11
+ changelog = os.path.normpath(os.path.join(__file__, "..", "CHANGELOG.md"))
12
12
  with open(changelog, "r", encoding="utf-8") as fh:
13
13
  long_description += fh.read()
14
14
 
15
15
 
16
16
  setuptools.setup(
17
-
18
17
  name="libreflow.extensions.sk.export_psd_layers",
19
18
  version=versioneer.get_version(),
20
19
  cmdclass=versioneer.get_cmdclass(),
@@ -32,15 +31,11 @@ setuptools.setup(
32
31
  "Operating System :: OS Independent",
33
32
  ],
34
33
  keywords="kabaret libreflow",
35
- install_requires=[],
36
- python_requires='>=3.7',
34
+ install_requires=["psd-tools"],
35
+ python_requires=">=3.7",
37
36
  packages=setuptools.find_packages("src"),
38
37
  package_dir={"": "src"},
39
38
  package_data={
40
- '': [
41
- "*.css",
42
- '*.png'
43
- ],
39
+ "": ["*.css", "*.png", "*.jsx"],
44
40
  },
45
-
46
41
  )
@@ -0,0 +1,598 @@
1
+ import os
2
+ import json
3
+ import gazu
4
+ import time
5
+
6
+ from kabaret import flow
7
+ from libreflow.flows.default.flow.film import Film
8
+ from libreflow.baseflow.file import (
9
+ GenericRunAction,
10
+ TrackedFile,
11
+ FileRevisionNameChoiceValue,
12
+ UploadPNGToKitsu,
13
+ PublishAndRenderPlayblast,
14
+ )
15
+ from psd_tools import PSDImage
16
+
17
+
18
+ class ExportPSDLayers(flow.Action):
19
+ ICON = ("icons.flow", "photoshop")
20
+
21
+ _file = flow.Parent()
22
+ _files = flow.Parent(2)
23
+ _shot = flow.Parent(5)
24
+ _sequence = flow.Parent(7)
25
+
26
+ revision = flow.Param(None, FileRevisionNameChoiceValue)
27
+
28
+ def allow_context(self, context):
29
+ return (
30
+ context
31
+ and self._file.format.get() in ["psd", "psb"]
32
+ and len(
33
+ self._file.get_revision_names(
34
+ sync_status="Available", published_only=True
35
+ )
36
+ ) > 0
37
+ )
38
+
39
+ def needs_dialog(self):
40
+ return True
41
+
42
+ def get_buttons(self):
43
+ self.revision.revert_to_default()
44
+ return ["Export", "Cancel"]
45
+
46
+ def ensure_render_folder(self):
47
+ folder_name = self._file.complete_name.get()
48
+ folder_name += "_render"
49
+
50
+ if not self._files.has_folder(folder_name):
51
+ self._files.create_folder_action.folder_name.set(folder_name)
52
+ self._files.create_folder_action.category.set("Outputs")
53
+ self._files.create_folder_action.tracked.set(True)
54
+ self._files.create_folder_action.run(None)
55
+
56
+ return self._files[folder_name]
57
+
58
+ def ensure_render_folder_revision(self):
59
+ folder = self.ensure_render_folder()
60
+ revision_name = self.revision.get()
61
+ source_revision = self._file.get_revision(self.revision.get())
62
+
63
+ if not folder.has_revision(revision_name):
64
+ revision = folder.add_revision(revision_name)
65
+ folder.set_current_user_on_revision(revision_name)
66
+ else:
67
+ revision = folder.get_revision(revision_name)
68
+
69
+ revision.comment.set(source_revision.comment.get())
70
+
71
+ folder.ensure_last_revision_oid()
72
+
73
+ self._files.touch()
74
+
75
+ return revision
76
+
77
+ def run(self, button):
78
+ if button == "Cancel":
79
+ return
80
+
81
+ session = self.root().session()
82
+ log_format = "[EXPORT LAYERS] {message}"
83
+
84
+ # Start log message
85
+ session.log_info(
86
+ log_format.format(
87
+ message=f"Export started - {self._sequence.name()} {self._shot.name()} {self._file.name()} {self.revision.get()}"
88
+ )
89
+ )
90
+
91
+ # Get revisions
92
+ source_revision = self._file.get_revision(self.revision.get())
93
+ render_revision = self.ensure_render_folder_revision()
94
+
95
+ # Open photoshop file
96
+ psb = PSDImage.open(source_revision.get_path())
97
+
98
+ # JSON structure for layers order
99
+ layers_data = {
100
+ "from": os.path.basename(source_revision.get_path()),
101
+ "layers": [],
102
+ "hidden_layers": []
103
+ }
104
+
105
+ # Export image layers
106
+ folder_name = os.path.basename(render_revision.get_path())
107
+
108
+ for layer in reversed(psb):
109
+ output_path = os.path.join(
110
+ render_revision.get_path(),
111
+ "{folder}-{layer}.png".format(folder=folder_name, layer=layer.name),
112
+ )
113
+
114
+ session.log_info(log_format.format(message=f'Exporting layer {layer.name}'))
115
+
116
+ image = layer.composite(viewport=psb.bbox)
117
+ image.save(output_path)
118
+
119
+ # Push layer in correct JSON data
120
+ layers_data["layers" if layer.visible else "hidden_layers"].append(
121
+ layer.name
122
+ )
123
+
124
+ # Export JSON data
125
+ json_object = json.dumps(layers_data)
126
+ json_path = os.path.join(render_revision.get_path(), "layers.json")
127
+
128
+ session.log_info(log_format.format(message='Saving layers.json'))
129
+ with open(json_path, "w") as outfile:
130
+ outfile.write(json_object)
131
+
132
+ session.log_info(log_format.format(message='Export complete'))
133
+
134
+ return self.get_result(close=True)
135
+
136
+
137
+ class SequencesSelectAll(flow.values.SessionValue):
138
+ DEFAULT_EDITOR = "bool"
139
+
140
+ _action = flow.Parent()
141
+
142
+ def _fill_ui(self, ui):
143
+ super(SequencesSelectAll, self)._fill_ui(ui)
144
+ if self._action.sequences.choices() == []:
145
+ ui["hidden"] = True
146
+
147
+
148
+ class SequencesMultiChoiceValue(flow.values.SessionValue):
149
+ DEFAULT_EDITOR = "multichoice"
150
+
151
+ _action = flow.Parent()
152
+
153
+ def choices(self):
154
+ return self._action._film.sequences.mapped_names()
155
+
156
+ def revert_to_default(self):
157
+ self.choices()
158
+ self.set([])
159
+
160
+
161
+ class TaskChoiceValue(flow.values.SessionValue):
162
+ DEFAULT_EDITOR = "choice"
163
+
164
+ _action = flow.Parent()
165
+
166
+ def choices(self):
167
+ return ['Any', 'BG_Layout', 'BG_Color']
168
+
169
+
170
+ class ExportPSDLayersBatch(flow.Action):
171
+ ICON = ("icons.flow", "photoshop")
172
+
173
+ select_all = (
174
+ flow.SessionParam(False, SequencesSelectAll).ui(editor="bool").watched()
175
+ )
176
+ sequences = flow.SessionParam([], SequencesMultiChoiceValue)
177
+ task_target = flow.SessionParam("Any", TaskChoiceValue)
178
+
179
+ _film = flow.Parent()
180
+
181
+ def allow_context(self, context):
182
+ user = self.root().project().get_user()
183
+ return user.status.get() == "Admin"
184
+
185
+ def get_buttons(self):
186
+ return ["Export", "Close"]
187
+
188
+ def needs_dialog(self):
189
+ self.message.set("<h2>Batch Export Layers</h2>")
190
+ return True
191
+
192
+ def child_value_changed(self, child_value):
193
+ if child_value is self.select_all:
194
+ if child_value.get():
195
+ self.sequences.set(self.sequences.choices())
196
+ else:
197
+ self.sequences.revert_to_default()
198
+
199
+ def run(self, button):
200
+ if button == "Close":
201
+ return
202
+
203
+ session = self.root().session()
204
+ log_format = "[BATCH EXPORT LAYERS] {status} - {sequence} {shot} {file} {revision}"
205
+
206
+ for seq_name in self.sequences.get():
207
+ seq = self._film.sequences[seq_name]
208
+ for shot in seq.shots.mapped_items():
209
+ for task in shot.tasks.mapped_items():
210
+ # Ignore if specific task parameter is used
211
+ if (
212
+ self.task_target.get() != "Any"
213
+ and task.name() != self.task_target.get().lower()
214
+ ):
215
+ continue
216
+ for f in task.files.mapped_items():
217
+ # Get only photoshop files
218
+ if (
219
+ f.format.get() in ["psd", "psb"]
220
+ and len(
221
+ f.get_revision_names(
222
+ sync_status="Available", published_only=True
223
+ )
224
+ ) > 0
225
+ ):
226
+ # Check if revision is already exported
227
+ if task.files.has_folder(f'{task.name()}_render'):
228
+ render_folder = task.files[f'{task.name()}_render']
229
+ render_revision = render_folder.get_head_revision(sync_status="Available")
230
+ if render_revision and os.path.exists(render_revision.get_path()):
231
+ session.log_warning(
232
+ log_format.format(
233
+ status="Already exported",
234
+ sequence=seq.name(),
235
+ shot=shot.name(),
236
+ file=f.display_name.get(),
237
+ revision=render_revision.name()
238
+ )
239
+ )
240
+ continue
241
+
242
+ # Start export base action
243
+ f.export_layers.revision.revert_to_default()
244
+ f.export_layers.run("Export")
245
+
246
+ # Wait for base action to finish
247
+ for sp in (
248
+ self.root()
249
+ .session()
250
+ .cmds.SubprocessManager.list_runner_infos()
251
+ ):
252
+ if sp["is_running"] and sp["label"] == "Export Layers":
253
+ while sp["is_running"]:
254
+ time.sleep(1)
255
+ sp = (
256
+ self.root()
257
+ .session()
258
+ .cmds.SubprocessManager.get_runner_info(
259
+ sp["id"]
260
+ )
261
+ )
262
+ break
263
+
264
+ # Upload render to exchange
265
+ render_folder = f.export_layers.ensure_render_folder()
266
+ render_revision = render_folder.get_head_revision(sync_status="Available")
267
+
268
+ if render_revision:
269
+ render_revision.upload.run('Upload')
270
+
271
+ session.log_info('[BATCH EXPORT LAYERS] Batch complete')
272
+
273
+
274
+ class PublishandExportPSD(PublishAndRenderPlayblast):
275
+ ICON = ('icons.libreflow', 'publish')
276
+
277
+ def allow_context(self, context):
278
+ return (
279
+ context
280
+ and super(PublishandExportPSD, self).allow_context(context)
281
+ and self._file.format.get() in ["psd", "psb"]
282
+ )
283
+
284
+ def get_buttons(self):
285
+ return ["Publish and Export Preview", "Cancel"]
286
+
287
+ def _configure_and_render(self, revision_name, upload_after_publish):
288
+ export_preview = self._file.export_preview
289
+ export_preview.revision.set(revision_name)
290
+ export_preview.upload_to_kitsu.set(upload_after_publish)
291
+
292
+ return export_preview.run("Export")
293
+
294
+ def run(self, button):
295
+ if button == "Cancel":
296
+ return
297
+
298
+ project_settings = self.root().project().settings()
299
+ if (
300
+ self.comment.get() == ""
301
+ and not project_settings.optional_publish_comment.get()
302
+ ):
303
+ self.message.set(
304
+ "<h2>Publish</h2>Please enter a comment to describe your changes."
305
+ )
306
+ return self.get_result(close=False)
307
+
308
+ # Update parameter presets
309
+ self.update_presets()
310
+
311
+ # Publish
312
+ publish_action = self._file.publish_action
313
+ publish_action.publish_file(
314
+ self._file,
315
+ comment=self.comment.get(),
316
+ keep_editing=self.keep_editing.get(),
317
+ upload_after_publish=False,
318
+ )
319
+
320
+ # Playblast
321
+ ret = self._configure_and_render(
322
+ self._file.get_head_revision().name(), self.upload_after_publish.get()
323
+ )
324
+
325
+ return ret
326
+
327
+
328
+ class ExportPSDPreview(flow.Action):
329
+ ICON = ("icons.flow", "photoshop")
330
+
331
+ revision = flow.Param(None, FileRevisionNameChoiceValue)
332
+ upload_to_kitsu = flow.BoolParam(False)
333
+
334
+ _file = flow.Parent()
335
+ _shot = flow.Parent(5)
336
+ _sequence = flow.Parent(7)
337
+
338
+ def allow_context(self, context):
339
+ return (
340
+ context
341
+ and self._file.format.get() in ["psd", "psb"]
342
+ and len(
343
+ self._file.get_revision_names(
344
+ sync_status="Available", published_only=True
345
+ )
346
+ ) > 0
347
+ )
348
+
349
+ def needs_dialog(self):
350
+ self.revision.revert_to_default()
351
+ return True
352
+
353
+ def get_buttons(self):
354
+ return ["Export", "Cancel"]
355
+
356
+ def get_target_path(self):
357
+ rev = self._file.get_revision(self.revision.get())
358
+ return rev.get_path()
359
+
360
+ def export_full(self, psb, file_path):
361
+ self.root().session().log_info("Exporting full project")
362
+
363
+ output_path = f"{os.path.splitext(file_path)[0]}.png"
364
+
365
+ image = psb.composite()
366
+ image.save(output_path)
367
+
368
+ return output_path
369
+
370
+ def export_no_chara(self, psb, file_path):
371
+ self.root().session().log_info("Exporting project without characters")
372
+
373
+ output_path = f"{os.path.splitext(file_path)[0]}_no_chara.png"
374
+
375
+ image = psb.composite(
376
+ layer_filter=lambda layer: layer.is_visible()
377
+ and "character" not in layer.name
378
+ and "INFO" not in layer.name
379
+ and "REF" not in layer.name
380
+ )
381
+
382
+ image.save(output_path)
383
+
384
+ return output_path
385
+
386
+ def export_cropped(self, psb, file_path):
387
+ self.root().session().log_info("Exporting project in film format")
388
+
389
+ output_path = f"{os.path.splitext(file_path)[0]}_no_safety.png"
390
+
391
+ frame_bbox = (0, 0, 0, 0)
392
+
393
+ frame_layer = psb.find("FRAME 4096x1716")
394
+
395
+ if frame_layer is None:
396
+ raise Exception("FRAME LAYER NOT FOUND")
397
+ else:
398
+ frame_bbox = frame_layer.bbox
399
+
400
+ if frame_bbox == (0, 0, 0, 0):
401
+ raise Exception("Frame Layer has no bounding box")
402
+
403
+ # Substract by 10pixels (width and height) in order to remove border margin
404
+ frame_bbox = (
405
+ frame_bbox[0] + 5,
406
+ frame_bbox[1] + 5,
407
+ frame_bbox[2] - 5,
408
+ frame_bbox[3] - 5,
409
+ )
410
+
411
+ image = psb.composite()
412
+ image = image.crop(frame_bbox)
413
+ image.save(output_path)
414
+
415
+ return output_path
416
+
417
+ def run(self, button):
418
+ if button == "Cancel":
419
+ return
420
+
421
+ path = self.get_target_path()
422
+ psb = PSDImage.open(path)
423
+
424
+ full_img_path = self.export_full(psb, path)
425
+ no_chara_img_path = self.export_no_chara(psb, path)
426
+ cropped_img_path = self.export_cropped(psb, path)
427
+
428
+ if self.upload_to_kitsu.get():
429
+ self._file.upload_preview.full_img_path.set(full_img_path)
430
+ self._file.upload_preview.no_chara_img_path.set(no_chara_img_path)
431
+ self._file.upload_preview.cropped_img_path.set(cropped_img_path)
432
+ self._file.upload_preview.revision_name.set(self.revision.get())
433
+ return self.get_result(next_action=self._file.upload_preview.oid())
434
+
435
+
436
+ class UploadPSDPreview(UploadPNGToKitsu):
437
+ _file = flow.Parent()
438
+
439
+ full_img_path = flow.Param("").ui(hidden=True)
440
+ no_chara_img_path = flow.Param("").ui(hidden=True)
441
+ cropped_img_path = flow.Param("").ui(hidden=True)
442
+
443
+ def allow_context(self, context):
444
+ return context and context.endswith(".details")
445
+
446
+ def upload_preview(
447
+ self,
448
+ kitsu_entity,
449
+ task_type_name,
450
+ task_status_name,
451
+ path_list,
452
+ comment="",
453
+ user_name=None,
454
+ ):
455
+ kitsu_api = self.root().project().kitsu_api()
456
+
457
+ # Get user
458
+ user = kitsu_api.get_user(user_name)
459
+
460
+ # Get task
461
+ task = kitsu_api.get_task(kitsu_entity, task_type_name)
462
+
463
+ if task is None or user is None:
464
+ return False
465
+
466
+ # Add comment with preview
467
+
468
+ # Check if preview file exists
469
+ for file_path in path_list:
470
+ if not os.path.exists(file_path):
471
+ self.root().session().log_error(
472
+ f"Preview file '{file_path}' does not exists."
473
+ )
474
+ return False
475
+
476
+ task_status = gazu.task.get_task_status_by_name(task_status_name)
477
+
478
+ # Check if status is valid
479
+ if task_status is None:
480
+ task_statuses = gazu.task.all_task_statuses()
481
+ names = [ts["name"] for ts in task_statuses]
482
+ self.root().session().log_error(
483
+ (
484
+ f"Invalid task status '{task_status_name}'."
485
+ "Should be one of " + str(names) + "."
486
+ )
487
+ )
488
+ return False
489
+
490
+ comment = gazu.task.add_comment(task, task_status, comment=comment)
491
+
492
+ for file_path in path_list:
493
+ try:
494
+ gazu.task.add_preview(task, comment, file_path)
495
+ except json.decoder.JSONDecodeError:
496
+ self.root().session().log_warning(
497
+ f"Invalid response from Gazu while uploading preview {file_path}"
498
+ )
499
+
500
+ return True
501
+
502
+ def run(self, button):
503
+ if button == "Cancel":
504
+ return
505
+
506
+ self.update_presets()
507
+
508
+ if not self._check_kitsu_params():
509
+ self.root().session().log_error("KITSU PARAM ERROR")
510
+ return self.get_result(close=False)
511
+
512
+ kitsu_api = self.root().project().kitsu_api()
513
+ kitsu_entity = self._ensure_kitsu_entity()
514
+
515
+ if kitsu_entity is None:
516
+ self.root().session().log_error(
517
+ "No Kitsu entity for file " + self._file.oid()
518
+ )
519
+ return self.get_result(close=False)
520
+
521
+ task_status_data = kitsu_api.get_task_status(
522
+ short_name=self.target_task_status.names_dict[self.target_task_status.get()]
523
+ )
524
+
525
+ success = self.upload_preview(
526
+ kitsu_entity=kitsu_entity,
527
+ task_type_name=self.target_task_type.get(),
528
+ task_status_name=task_status_data["name"],
529
+ path_list=[
530
+ self.full_img_path.get(),
531
+ self.no_chara_img_path.get(),
532
+ self.cropped_img_path.get(),
533
+ ],
534
+ comment=self.comment.get(),
535
+ )
536
+
537
+ if not success:
538
+ self.message.set(
539
+ (
540
+ "<h2>Upload playblast to Kitsu</h2>"
541
+ "<font color=#FF584D>An error occured "
542
+ "while uploading the preview.</font>"
543
+ )
544
+ )
545
+ return self.get_result(close=False)
546
+
547
+ rev = self._file.get_revision(self.revision_name.get())
548
+ rev.set_status("on_kitsu")
549
+
550
+
551
+ def publish_and_export_preview(parent):
552
+ if isinstance(parent, TrackedFile):
553
+ r = flow.Child(PublishandExportPSD)
554
+ r.name = "publish_and_export_preview"
555
+ r.index = 26
556
+ return r
557
+
558
+
559
+ def upload_preview(parent):
560
+ if isinstance(parent, TrackedFile):
561
+ r = flow.Child(UploadPSDPreview)
562
+ r.name = "upload_preview"
563
+ return r
564
+
565
+
566
+ def export_psd_layers(parent):
567
+ if isinstance(parent, TrackedFile):
568
+ r = flow.Child(ExportPSDLayers)
569
+ r.name = "export_layers"
570
+ r.index = 50
571
+ return r
572
+
573
+ if isinstance(parent, Film):
574
+ r = flow.Child(ExportPSDLayersBatch).ui(
575
+ label="Batch Export Layers", dialog_size=(750, 800)
576
+ )
577
+ r.name = "export_layers_batch"
578
+ r.index = None
579
+ return r
580
+
581
+
582
+ def export_psd_preview(parent):
583
+ if isinstance(parent, TrackedFile):
584
+ r = flow.Child(ExportPSDPreview)
585
+ r.name = "export_preview"
586
+ r.index = 49
587
+ return r
588
+
589
+
590
+ def install_extensions(session):
591
+ return {
592
+ "export_psd_layers": [
593
+ export_psd_layers,
594
+ export_psd_preview,
595
+ upload_preview,
596
+ publish_and_export_preview,
597
+ ]
598
+ }
@@ -8,11 +8,11 @@ import json
8
8
 
9
9
  version_json = '''
10
10
  {
11
- "date": "2025-03-27T12:06:44+0100",
11
+ "date": "2025-04-24T19:49:41+0200",
12
12
  "dirty": false,
13
13
  "error": null,
14
- "full-revisionid": "14117b39d39bffa801addc037065b58ad52037c8",
15
- "version": "1.0.0"
14
+ "full-revisionid": "29231b33e0e266c52c6065cb665d5fbbaf827fdf",
15
+ "version": "1.1.0"
16
16
  }
17
17
  ''' # END VERSION_JSON
18
18
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: libreflow.extensions.sk.export_psd_layers
3
- Version: 1.0.0
3
+ Version: 1.1.0
4
4
  Home-page: https://gitlab.com/lfs.coop/libreflow/libreflow_launcher
5
5
  Author: Thomas Thiebaut
6
6
  Author-email: autor@les-fees-speciales.coop
@@ -12,6 +12,7 @@ Classifier: License :: OSI Approved :: GNU Lesser General Public License v3 or l
12
12
  Classifier: Operating System :: OS Independent
13
13
  Requires-Python: >=3.7
14
14
  Description-Content-Type: text/markdown
15
+ Requires-Dist: psd-tools
15
16
  Dynamic: author
16
17
  Dynamic: author-email
17
18
  Dynamic: classifier
@@ -20,6 +21,7 @@ Dynamic: description-content-type
20
21
  Dynamic: home-page
21
22
  Dynamic: keywords
22
23
  Dynamic: license
24
+ Dynamic: requires-dist
23
25
  Dynamic: requires-python
24
26
 
25
27
  # Sk Export Psd Layers
@@ -48,6 +50,34 @@ Types of changes
48
50
 
49
51
  ## [Unreleased]
50
52
 
53
+ ## [1.1.0] - 2025-04-24
54
+
55
+ ### Added
56
+
57
+ * An action to export a psd for preview
58
+ * Use the `psd-tools` python module
59
+ * Three PNGs, one in full format, one with no characters and one without a safety margin
60
+ * These files are not tracked in Libreflow, they remain local
61
+ * A specific upload to kitsu action is available for this use case
62
+ * An action to publish and export at the same time
63
+ * An action to export layers in batch
64
+ * Available at a film hierarchy level
65
+
66
+ ### Changed
67
+
68
+ * Export layers action now uses `psd-tools` python module instead of a Adobe extend script
69
+ * Process can takes longer, but Photoshop is no longer required
70
+
71
+ ### Fixed
72
+
73
+ * Hide export layers action when file has no published revisions
74
+
75
+ ## [1.0.1] - 2025-04-04
76
+
77
+ ### Fixed
78
+
79
+ * including .jsx files in setup.py
80
+
51
81
  ## [1.0.0] - 2025-03-27
52
82
 
53
83
  ### Added
@@ -8,6 +8,7 @@ src/libreflow/__init__.py
8
8
  src/libreflow.extensions.sk.export_psd_layers.egg-info/PKG-INFO
9
9
  src/libreflow.extensions.sk.export_psd_layers.egg-info/SOURCES.txt
10
10
  src/libreflow.extensions.sk.export_psd_layers.egg-info/dependency_links.txt
11
+ src/libreflow.extensions.sk.export_psd_layers.egg-info/requires.txt
11
12
  src/libreflow.extensions.sk.export_psd_layers.egg-info/top_level.txt
12
13
  src/libreflow/extensions/__init__.py
13
14
  src/libreflow/extensions/sk/__init__.py
@@ -1,26 +0,0 @@
1
- # Changelog
2
-
3
- All notable changes to this project will be documented in this file.
4
-
5
- The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
6
- and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html)[^1].
7
-
8
- <!---
9
- Types of changes
10
-
11
- - Added for new features.
12
- - Changed for changes in existing functionality.
13
- - Deprecated for soon-to-be removed features.
14
- - Removed for now removed features.
15
- - Fixed for any bug fixes.
16
- - Security in case of vulnerabilities.
17
-
18
- -->
19
-
20
- ## [Unreleased]
21
-
22
- ## [1.0.0] - 2025-03-27
23
-
24
- ### Added
25
-
26
- * Extension to export the layers of a Photoshop project as png images
@@ -1,125 +0,0 @@
1
- import os
2
-
3
- from kabaret import flow
4
- from kabaret.flow.object import _Manager
5
-
6
- from libreflow.baseflow.file import GenericRunAction,TrackedFile,TrackedFolder,FileRevisionNameChoiceValue
7
-
8
- class ExportPSDLayers(GenericRunAction):
9
- _MANAGER_TYPE = _Manager
10
-
11
- ICON = ('icons.flow', 'photoshop')
12
-
13
- _file = flow.Parent()
14
- _files = flow.Parent(2)
15
- _task = flow.Parent(3)
16
- _shot = flow.Parent(5)
17
- _sequence = flow.Parent(7)
18
-
19
- revision = flow.Param(None, FileRevisionNameChoiceValue)
20
-
21
- exec_path = ""
22
-
23
- def allow_context(self, context):
24
- return (
25
- context
26
- and self._file.format.get() in ['psd', 'psb']
27
- )
28
-
29
- def needs_dialog(self):
30
- return True
31
-
32
- def get_buttons(self):
33
- self.revision.revert_to_default()
34
-
35
- msg = ""
36
-
37
- site_env = self.root().project().get_current_site().site_environment
38
- if site_env.has_mapped_name('PHOTOSHOP_EXEC_PATH'):
39
- self.exec_path = site_env['PHOTOSHOP_EXEC_PATH'].value.get()
40
- self.message.set(msg)
41
- return ['Export','Cancel']
42
-
43
- else :
44
- msg = "<font color = red><b>Photoshop executable not found in site environment</b></font>"
45
- self.message.set(msg)
46
- return ['Cancel']
47
-
48
- def ensure_render_folder(self):
49
- folder_name = self._file.display_name.get().split('.')[0]
50
- folder_name += '_render'
51
-
52
- if not self._files.has_folder(folder_name):
53
- self._files.create_folder_action.folder_name.set(folder_name)
54
- self._files.create_folder_action.category.set('Outputs')
55
- self._files.create_folder_action.tracked.set(True)
56
- self._files.create_folder_action.run(None)
57
-
58
- return self._files[folder_name]
59
-
60
- def ensure_render_folder_revision(self):
61
- folder = self.ensure_render_folder()
62
- revision_name = self.revision.get()
63
- revisions = folder.get_revisions()
64
- source_revision = self._file.get_revision(self.revision.get())
65
-
66
- if not folder.has_revision(revision_name):
67
- revision = folder.add_revision(revision_name)
68
- folder.set_current_user_on_revision(revision_name)
69
- else:
70
- revision = folder.get_revision(revision_name)
71
-
72
- revision.comment.set(source_revision.comment.get())
73
-
74
- folder.ensure_last_revision_oid()
75
-
76
- self._files.touch()
77
-
78
- return revision
79
-
80
- def runner_name_and_tags(self):
81
- return 'Photoshop', []
82
-
83
- def get_run_label(self):
84
- return 'Export Layers'
85
-
86
- def target_file_extension(self):
87
- if self._file.format.get() == 'psb':
88
- return 'psb'
89
- elif self._file.format.get() == 'psd':
90
- return 'psd'
91
-
92
- def extra_argv(self):
93
- rev = self._file.get_revision(self.revision.get())
94
- current_dir = os.path.split(__file__)[0]
95
- script_path = os.path.normpath(os.path.join(current_dir,"scripts/LFS_PSD_export_to_PNG.jsx"))
96
-
97
- return [rev.get_path(),script_path]
98
-
99
- def run(self, button):
100
- if button == 'Cancel':
101
- return
102
-
103
- folder_path = self.ensure_render_folder_revision().get_path()
104
-
105
- super(ExportPSDLayers, self).run(button)
106
- return self.get_result(close=True)
107
-
108
-
109
- def export_psd_layers(parent):
110
- if isinstance(parent, TrackedFile):
111
- r = flow.Child(ExportPSDLayers)
112
- r.name = 'export_layers'
113
- return r
114
-
115
-
116
- def install_extensions(session):
117
- return {
118
- "export_psd_layers": [
119
- export_psd_layers,
120
- ]
121
- }
122
-
123
-
124
- from . import _version
125
- __version__ = _version.get_versions()['version']