11
11
from pymol .Qt import QtGui
12
12
from pymol .Qt import QtWidgets
13
13
14
+ from dataclasses import dataclass
15
+
14
16
import os
15
17
from io import BytesIO
18
+ from pathlib import Path
16
19
17
20
from PIL import Image
18
21
from PIL import ImageChops
19
22
from PIL import ImageDraw
20
23
from PIL import ImageFilter
21
24
from PIL import ImageOps
22
25
23
- __version__ = "0.1 "
26
+ __version__ = "0.2 "
24
27
25
28
26
29
def __init_plugin__ (app = None ) -> None :
@@ -100,23 +103,33 @@ def _replace_color(img: Image, target_color: tuple,
100
103
return edges
101
104
102
105
106
+ @dataclass
107
+ class Extent2D :
108
+ width : int = 0
109
+ height : int = 0
110
+
111
+
103
112
def _outline (outline_sele : str , outline_color : tuple , outline_width : int ,
104
- scale : int , reps : tuple ) -> None :
113
+ scale : int , reps : tuple , image_extent : Extent2D ,
114
+ save_file : bool ) -> None :
105
115
"""
106
116
Outline a selection's representations with a specific color.
107
117
:param outline_sele: Selection to outline
108
118
:param outline_color: Color to outline with
109
119
:param outline_width: Width of outline
110
120
:param scale: Scale factor for antialiasing
111
121
:param reps: Representations to outline
122
+ :param image_extent: Image extent
123
+ :param save_file: Should save to file
112
124
"""
113
125
try :
114
126
tmp_scene = "tmp_scene"
115
127
116
128
cmd .scene (tmp_scene , "store" , quiet = 1 )
117
129
118
130
# Render what we have
119
- base_bytes = cmd .png (filename = None , ray = 1 )
131
+ width , height = image_extent .width , image_extent .height
132
+ base_bytes = cmd .png (filename = None , ray = 1 , width = width , height = height )
120
133
121
134
# Render only whats outlined
122
135
cmd .hide ('everything' )
@@ -138,7 +151,6 @@ def _outline(outline_sele: str, outline_color: tuple, outline_width: int,
138
151
139
152
ray_antialias = cmd .get ('antialias' )
140
153
cmd .set ('antialias' , 0 )
141
- (width , height ) = cmd .get_viewport ()
142
154
overlay_bytes = cmd .png (filename = None , ray = 1 , width = width * scale ,
143
155
height = height * scale )
144
156
@@ -149,10 +161,26 @@ def _outline(outline_sele: str, outline_color: tuple, outline_width: int,
149
161
150
162
composite = Image .composite (overlay , base , overlay )
151
163
164
+ composition_file = Path .cwd () / "_tmp_outline_comp.png"
165
+ composition_file_name = str (composition_file )
166
+ composite .save (composition_file_name )
152
167
# TODO: load_png doesn't take raw bytes so we have to save to disk
153
- tmp_composite_png = "_tmp_outline_comp.png"
154
- composite .save (tmp_composite_png )
155
- cmd .load_png (tmp_composite_png , quiet = 1 )
168
+ cmd .load_png (composition_file_name , quiet = 1 )
169
+ if save_file :
170
+ new_name = QtWidgets .QFileDialog .getSaveFileName (
171
+ None , "Save File" , str (Path .cwd ()), "PNG Files (*.png)" )[0 ]
172
+ # rename file
173
+ new_path = Path (new_name )
174
+ if new_path .exists ():
175
+ new_path .unlink ()
176
+ composition_file .rename (new_path )
177
+ composition_file_name = str (new_path )
178
+ msg = QtWidgets .QMessageBox ()
179
+ msg .setWindowTitle ("File Saved" )
180
+ msg .setText (f"Saved to { composition_file_name } " )
181
+ msg .setStandardButtons (QtWidgets .QMessageBox .Ok )
182
+ msg .exec_ ()
183
+
156
184
finally :
157
185
# Revert scene and clean up
158
186
cmd .scene (tmp_scene , "recall" , quiet = 1 )
@@ -162,7 +190,8 @@ def _outline(outline_sele: str, outline_color: tuple, outline_width: int,
162
190
cmd .set ('ray_trace_color' , ray_trace_color )
163
191
cmd .set ('bg_rgb' , bg_color )
164
192
cmd .set ('ray_opaque_background' , ray_opaque_background )
165
- os .remove (tmp_composite_png )
193
+ if not save_file :
194
+ composition_file .unlink ()
166
195
167
196
168
197
class StringListSelectorWidgetItem (QtWidgets .QWidget ):
@@ -251,6 +280,34 @@ def addButton(self, label: str) -> QtWidgets.QRadioButton:
251
280
return button
252
281
253
282
283
+ class ImageExtentSelectionGroup (QtWidgets .QWidget ):
284
+ """
285
+ Two spinboxes for selecting image width and height
286
+ """
287
+ def __init__ (self , * args , ** kwargs ) -> None :
288
+ super ().__init__ (* args , ** kwargs )
289
+ width , height = cmd .get_viewport ()
290
+ self .width = width
291
+ self .height = height
292
+ self .width_box = QtWidgets .QSpinBox ()
293
+ self .height_box = QtWidgets .QSpinBox ()
294
+ self .width_box .setRange (32 , 8192 )
295
+ self .height_box .setRange (32 , 8192 )
296
+ self .width_box .setValue (self .width )
297
+ self .height_box .setValue (self .height )
298
+ self .layout = QtWidgets .QHBoxLayout (self )
299
+ self .layout .addWidget (QtWidgets .QLabel ("Img Width:" ))
300
+ self .layout .addWidget (self .width_box )
301
+ self .layout .addWidget (QtWidgets .QLabel ("Img Height:" ))
302
+ self .layout .addWidget (self .height_box )
303
+
304
+ def get_extent (self ) -> Extent2D :
305
+ """
306
+ :return: Image extent
307
+ """
308
+ return Extent2D (self .width_box .value (), self .height_box .value ())
309
+
310
+
254
311
class RepresentationOutlineDialog (QtWidgets .QDialog ):
255
312
"""
256
313
Representation Outline Dialog that allows the user to outline a selection's
@@ -322,6 +379,12 @@ def __init__(self, *args, **kwargs) -> None:
322
379
self .antialias_layout .addWidget (self .low_aa )
323
380
self .antialias_layout .addWidget (self .hi_aa )
324
381
382
+ # Image Extent Spinbox group
383
+ self .image_extent_selection_group = ImageExtentSelectionGroup ()
384
+
385
+ # File save field
386
+ self .save_file_checkbox = QtWidgets .QCheckBox ("Save to file" )
387
+
325
388
self ._updateCol ()
326
389
327
390
# Brief note
@@ -337,6 +400,8 @@ def __init__(self, *args, **kwargs) -> None:
337
400
self .layout .addWidget (self .color_dialogue_btn )
338
401
self .layout .addLayout (self .slider_layout )
339
402
self .layout .addLayout (self .antialias_layout )
403
+ self .layout .addWidget (self .image_extent_selection_group )
404
+ self .layout .addWidget (self .save_file_checkbox )
340
405
self .layout .addWidget (self .outline_button )
341
406
self .layout .addWidget (self .note )
342
407
@@ -381,9 +446,12 @@ def onComboChanged():
381
446
def onOutlineClicked ():
382
447
col = self .color_dialogue .currentColor ().getRgb ()
383
448
scale = int (self .antialias_group .checkedButton ().text ()[0 ])
384
- width = self ._kernelToWidth (self .width_slider .value () * scale )
449
+ outline_width = self ._kernelToWidth (self .width_slider .value () * scale )
450
+ image_extent = self .image_extent_selection_group .get_extent ()
385
451
_outline (self .combobox .currentText (), col ,
386
- width , scale , self .rep_list .get_rep_list ())
452
+ outline_width , scale , self .rep_list .get_rep_list (),
453
+ image_extent ,
454
+ self .save_file_checkbox .isChecked ())
387
455
388
456
self .outline_button .clicked .connect (onOutlineClicked )
389
457
0 commit comments