28e5e59d7803e922f864b2eeecedb3233276d00e
[blender.git] / release / scripts / startup / bl_operators / image.py
1 # ##### BEGIN GPL LICENSE BLOCK #####
2 #
3 #  This program is free software; you can redistribute it and/or
4 #  modify it under the terms of the GNU General Public License
5 #  as published by the Free Software Foundation; either version 2
6 #  of the License, or (at your option) any later version.
7 #
8 #  This program is distributed in the hope that it will be useful,
9 #  but WITHOUT ANY WARRANTY; without even the implied warranty of
10 #  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
11 #  GNU General Public License for more details.
12 #
13 #  You should have received a copy of the GNU General Public License
14 #  along with this program; if not, write to the Free Software Foundation,
15 #  Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
16 #
17 # ##### END GPL LICENSE BLOCK #####
18
19 # <pep8-80 compliant>
20
21 import bpy
22 from bpy.types import Operator
23 from bpy.props import StringProperty
24
25
26 class EditExternally(Operator):
27     """Edit image in an external application"""
28     bl_idname = "image.external_edit"
29     bl_label = "Image Edit Externally"
30     bl_options = {'REGISTER'}
31
32     filepath: StringProperty(
33         subtype='FILE_PATH',
34     )
35
36     @staticmethod
37     def _editor_guess(context):
38         import sys
39
40         image_editor = context.user_preferences.filepaths.image_editor
41
42         # use image editor in the preferences when available.
43         if not image_editor:
44             if sys.platform[:3] == "win":
45                 image_editor = ["start"]  # not tested!
46             elif sys.platform == "darwin":
47                 image_editor = ["open"]
48             else:
49                 image_editor = ["gimp"]
50         else:
51             if sys.platform == "darwin":
52                 # blender file selector treats .app as a folder
53                 # and will include a trailing backslash, so we strip it.
54                 image_editor.rstrip('\\')
55                 image_editor = ["open", "-a", image_editor]
56             else:
57                 image_editor = [image_editor]
58
59         return image_editor
60
61     def execute(self, context):
62         import os
63         import subprocess
64
65         filepath = self.filepath
66
67         if not filepath:
68             self.report({'ERROR'}, "Image path not set")
69             return {'CANCELLED'}
70
71         if not os.path.exists(filepath) or not os.path.isfile(filepath):
72             self.report({'ERROR'},
73                         "Image path %r not found, image may be packed or "
74                         "unsaved" % filepath)
75             return {'CANCELLED'}
76
77         cmd = self._editor_guess(context) + [filepath]
78
79         try:
80             subprocess.Popen(cmd)
81         except:
82             import traceback
83             traceback.print_exc()
84             self.report({'ERROR'},
85                         "Image editor could not be launched, please ensure that "
86                         "the path in User Preferences > File is valid, and Blender has rights to launch it")
87
88             return {'CANCELLED'}
89
90         return {'FINISHED'}
91
92     def invoke(self, context, event):
93         import os
94         sd = context.space_data
95         try:
96             image = sd.image
97         except AttributeError:
98             self.report({'ERROR'}, "Context incorrect, image not found")
99             return {'CANCELLED'}
100
101         if image.packed_file:
102             self.report({'ERROR'}, "Image is packed, unpack before editing")
103             return {'CANCELLED'}
104
105         if sd.type == 'IMAGE_EDITOR':
106             filepath = image.filepath_from_user(sd.image_user)
107         else:
108             filepath = image.filepath
109
110         filepath = bpy.path.abspath(filepath, library=image.library)
111
112         self.filepath = os.path.normpath(filepath)
113         self.execute(context)
114
115         return {'FINISHED'}
116
117
118 class SaveDirty(Operator):
119     """Save all modified textures"""
120     bl_idname = "image.save_dirty"
121     bl_label = "Save Dirty"
122     bl_options = {'REGISTER', 'UNDO'}
123
124     def execute(self, context):
125         unique_paths = set()
126         for image in bpy.data.images:
127             if image.is_dirty:
128                 if image.packed_file:
129                     if image.library:
130                         self.report({'WARNING'},
131                                     "Packed library image: %r from library %r"
132                                     " can't be re-packed" %
133                                     (image.name, image.library.filepath))
134                     else:
135                         image.pack(as_png=True)
136                 else:
137                     filepath = bpy.path.abspath(image.filepath,
138                                                 library=image.library)
139                     if "\\" not in filepath and "/" not in filepath:
140                         self.report({'WARNING'}, "Invalid path: " + filepath)
141                     elif filepath in unique_paths:
142                         self.report({'WARNING'},
143                                     "Path used by more than one image: %r" %
144                                     filepath)
145                     else:
146                         unique_paths.add(filepath)
147                         image.save()
148         return {'FINISHED'}
149
150
151 class ProjectEdit(Operator):
152     """Edit a snapshot of the view-port in an external image editor"""
153     bl_idname = "image.project_edit"
154     bl_label = "Project Edit"
155     bl_options = {'REGISTER'}
156
157     _proj_hack = [""]
158
159     def execute(self, context):
160         import os
161
162         EXT = "png"  # could be made an option but for now ok
163
164         for image in bpy.data.images:
165             image.tag = True
166
167         # opengl buffer may fail, we can't help this, but best report it.
168         try:
169             bpy.ops.paint.image_from_view()
170         except RuntimeError as err:
171             self.report({'ERROR'}, str(err))
172             return {'CANCELLED'}
173
174         image_new = None
175         for image in bpy.data.images:
176             if not image.tag:
177                 image_new = image
178                 break
179
180         if not image_new:
181             self.report({'ERROR'}, "Could not make new image")
182             return {'CANCELLED'}
183
184         filepath = os.path.basename(bpy.data.filepath)
185         filepath = os.path.splitext(filepath)[0]
186         # fixes <memory> rubbish, needs checking
187         # filepath = bpy.path.clean_name(filepath)
188
189         if bpy.data.is_saved:
190             filepath = "//" + filepath
191         else:
192             tmpdir = context.user_preferences.filepaths.temporary_directory
193             filepath = os.path.join(tmpdir, "project_edit")
194
195         obj = context.object
196
197         if obj:
198             filepath += "_" + bpy.path.clean_name(obj.name)
199
200         filepath_final = filepath + "." + EXT
201         i = 0
202
203         while os.path.exists(bpy.path.abspath(filepath_final)):
204             filepath_final = filepath + ("%.3d.%s" % (i, EXT))
205             i += 1
206
207         image_new.name = bpy.path.basename(filepath_final)
208         ProjectEdit._proj_hack[0] = image_new.name
209
210         image_new.filepath_raw = filepath_final  # TODO, filepath raw is crummy
211         image_new.file_format = 'PNG'
212         image_new.save()
213
214         filepath_final = bpy.path.abspath(filepath_final)
215
216         try:
217             bpy.ops.image.external_edit(filepath=filepath_final)
218         except RuntimeError as re:
219             self.report({'ERROR'}, str(re))
220
221         return {'FINISHED'}
222
223
224 class ProjectApply(Operator):
225     """Project edited image back onto the object"""
226     bl_idname = "image.project_apply"
227     bl_label = "Project Apply"
228     bl_options = {'REGISTER'}
229
230     def execute(self, context):
231         image_name = ProjectEdit._proj_hack[0]  # TODO, deal with this nicer
232
233         try:
234             image = bpy.data.images[image_name, None]
235         except KeyError:
236             import traceback
237             traceback.print_exc()
238             self.report({'ERROR'}, "Could not find image '%s'" % image_name)
239             return {'CANCELLED'}
240
241         image.reload()
242         bpy.ops.paint.project_image(image=image_name)
243
244         return {'FINISHED'}
245
246
247 classes = (
248     EditExternally,
249     ProjectApply,
250     ProjectEdit,
251     SaveDirty,
252 )