''' Copyright (C) 2021 - 2024 Akaneyu This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . ''' import math import bpy import numpy as np from . import app from . import utils class IMAGE_EDITOR_PLUS_OT_make_selection(bpy.types.Operator): """Make a selection on the image""" bl_idname = "image_editor_plus.make_selection" bl_label = "Make Selection" def __init__(self): self.lmb = False def modal(self, context, event): area_session = app.get_area_session(context) context.area.tag_redraw() img = context.area.spaces.active.image width, height = img.size[0], img.size[1] if event.type == 'MOUSEMOVE': if self.lmb: region_pos = [event.mouse_region_x, event.mouse_region_y] if area_session.selection_region: area_session.selection_region[1] = region_pos else: area_session.selection_region = [region_pos, region_pos] elif event.type == 'LEFTMOUSE': if event.value == 'PRESS': self.lmb = True elif event.value == 'RELEASE': self.lmb = False if area_session.selection_region: area_session.selecting = False app.convert_selection(context) # deselect layer img_props = img.imageeditorplus_properties img_props.selected_layer_index = -1 return {'FINISHED'} elif event.type in {'RIGHTMOUSE', 'ESC'}: area_session.selection = None area_session.selection_region = None area_session.selecting = False return {'CANCELLED'} return {'RUNNING_MODAL'} def invoke(self, context, event): area_session = app.get_area_session(context) if context.area.type != 'IMAGE_EDITOR': return {'CANCELLED'} area_session.selection = None area_session.selection_region = None area_session.selecting = True context.window_manager.modal_handler_add(self) return {'RUNNING_MODAL'} class IMAGE_EDITOR_PLUS_OT_cancel_selection(bpy.types.Operator): """Cancel the selection""" bl_idname = "image_editor_plus.cancel_selection" bl_label = "Cancel Selection" def execute(self, context): area_session = app.get_area_session(context) if not area_session.selection: return {'CANCELLED'} app.cancel_selection(context) context.area.tag_redraw() return {'FINISHED'} class IMAGE_EDITOR_PLUS_OT_swap_colors(bpy.types.Operator): """Swap foreground and background color""" bl_idname = "image_editor_plus.swap_colors" bl_label = "Swap Colors" def execute(self, context): wm = context.window_manager props = wm.imageeditorplus_properties props.foreground_color, props.background_color = \ props.background_color[:], props.foreground_color[:] return {'FINISHED'} class IMAGE_EDITOR_PLUS_OT_fill_with_fg_color(bpy.types.Operator): """Fill the image with foreground color""" bl_idname = "image_editor_plus.fill_with_fg_color" bl_label = "Fill with FG Color" def execute(self, context): wm = context.window_manager props = wm.imageeditorplus_properties color = props.foreground_color[:] + (1.0,) img = app.get_target_image(context) if not img: return {'CANCELLED'} pixels = utils.read_pixels_from_image(img) selection = app.get_target_selection(context) if selection: pixels[selection[0][1]:selection[1][1], selection[0][0]:selection[1][0]] = color elif selection == []: return {'CANCELLED'} else: pixels[:] = color utils.write_pixels_to_image(img, pixels) app.refresh_image(context) return {'FINISHED'} class IMAGE_EDITOR_PLUS_OT_fill_with_bg_color(bpy.types.Operator): """Fill the image with background color""" bl_idname = "image_editor_plus.fill_with_bg_color" bl_label = "Fill with BG Color" def execute(self, context): wm = context.window_manager props = wm.imageeditorplus_properties color = props.background_color[:] + (1.0,) img = app.get_target_image(context) if not img: return {'CANCELLED'} pixels = utils.read_pixels_from_image(img) selection = app.get_target_selection(context) if selection: pixels[selection[0][1]:selection[1][1], selection[0][0]:selection[1][0]] = color elif selection == []: return {'CANCELLED'} else: pixels[:] = color utils.write_pixels_to_image(img, pixels) app.refresh_image(context) return {'FINISHED'} class IMAGE_EDITOR_PLUS_OT_clear(bpy.types.Operator): """Clear the image""" bl_idname = "image_editor_plus.clear" bl_label = "Clear" def execute(self, context): img = app.get_target_image(context) if not img: return {'CANCELLED'} pixels = utils.read_pixels_from_image(img) selection = app.get_target_selection(context) if selection: pixels[selection[0][1]:selection[1][1], selection[0][0]:selection[1][0]] = (0, 0, 0, 0) elif selection == []: return {'CANCELLED'} else: pixels[:] = (0, 0, 0, 0) utils.write_pixels_to_image(img, pixels) app.refresh_image(context) return {'FINISHED'} class IMAGE_EDITOR_PLUS_OT_cut(bpy.types.Operator): """Cut the image""" bl_idname = "image_editor_plus.cut" bl_label = "Cut" def execute(self, context): session = app.get_session() img = app.get_target_image(context) if not img: return {'CANCELLED'} width, height = img.size pixels = utils.read_pixels_from_image(img) selection = app.get_target_selection(context) if selection: target_pixels = pixels[selection[0][1]:selection[1][1], selection[0][0]:selection[1][0]] elif selection == []: return {'CANCELLED'} else: target_pixels = pixels session.copied_image_pixels = target_pixels.copy() session.copied_image_settings = { 'is_float': img.is_float, 'colorspace_name': img.colorspace_settings.name } layer = app.get_active_layer(context) if layer: session.copied_layer_settings = { 'rotation': layer.rotation, 'scale': layer.scale, 'custom_data': layer.custom_data } else: session.copied_layer_settings = None if selection: pixels[selection[0][1]:selection[1][1], selection[0][0]:selection[1][0]] = (0, 0, 0, 0) else: pixels[:] = (0, 0, 0, 0) utils.write_pixels_to_image(img, pixels) app.refresh_image(context) self.report({'INFO'}, 'Cut selected image.') return {'FINISHED'} class IMAGE_EDITOR_PLUS_OT_copy(bpy.types.Operator): """Copy the image""" bl_idname = "image_editor_plus.copy" bl_label = "Copy" def execute(self, context): session = app.get_session() img = app.get_target_image(context) if not img: return {'CANCELLED'} width, height = img.size pixels = utils.read_pixels_from_image(img) selection = app.get_target_selection(context) if selection: target_pixels = pixels[selection[0][1]:selection[1][1], selection[0][0]:selection[1][0]] elif selection == []: return {'CANCELLED'} else: target_pixels = pixels session.copied_image_pixels = target_pixels session.copied_image_settings = { 'is_float': img.is_float, 'colorspace_name': img.colorspace_settings.name } layer = app.get_active_layer(context) if layer: session.copied_layer_settings = { 'rotation': layer.rotation, 'scale': layer.scale, 'custom_data': layer.custom_data } else: session.copied_layer_settings = None self.report({'INFO'}, 'Copied selected image.') return {'FINISHED'} class IMAGE_EDITOR_PLUS_OT_paste(bpy.types.Operator): """Paste the image""" bl_idname = "image_editor_plus.paste" bl_label = "Paste" def execute(self, context): session = app.get_session() img = context.area.spaces.active.image if not img: return {'CANCELLED'} target_pixels = session.copied_image_pixels if target_pixels is None: return {'CANCELLED'} app.create_layer(img, target_pixels, session.copied_image_settings, session.copied_layer_settings) app.cancel_selection(context) app.refresh_image(context) return {'FINISHED'} class IMAGE_EDITOR_PLUS_OT_crop(bpy.types.Operator): """Crop the image to the boundary of the selection""" bl_idname = "image_editor_plus.crop" bl_label = "Crop" def execute(self, context): wm = context.window_manager img = context.area.spaces.active.image if not img: return {'CANCELLED'} pixels = utils.read_pixels_from_image(img) selection = app.get_selection(context) if selection: target_pixels = pixels[selection[0][1]:selection[1][1], selection[0][0]:selection[1][0]] else: target_pixels = pixels target_width, target_height = target_pixels.shape[1], target_pixels.shape[0] img.scale(target_width, target_height) utils.write_pixels_to_image(img, target_pixels) if selection: img_props = img.imageeditorplus_properties layers = img_props.layers for layer in reversed(layers): layer_pos = layer.location layer_pos[0] -= selection[0][0] layer_pos[1] -= selection[0][1] app.cancel_selection(context) app.refresh_image(context) return {'FINISHED'} class IMAGE_EDITOR_PLUS_OT_deselect_layer(bpy.types.Operator): bl_idname = "image_editor_plus.deselect_layer" bl_label = "Deselect Layer" def execute(self, context): img = context.area.spaces.active.image if not img: return {'CANCELLED'} img_props = img.imageeditorplus_properties img_props.selected_layer_index = -1 app.refresh_image(context) return {'FINISHED'} class IMAGE_EDITOR_PLUS_OT_move_layer(bpy.types.Operator): """Move the layer""" bl_idname = "image_editor_plus.move_layer" bl_label = "Move Layer" def __init__(self): self.start_input_position = [0, 0] self.start_layer_location = [0, 0] def modal(self, context, event): area_session = app.get_area_session(context) context.area.tag_redraw() img = context.area.spaces.active.image width, height = img.size[0], img.size[1] layer = app.get_active_layer(context) if not layer: return {'CANCELLED'} layer_width, layer_height = 1, 1 layer_img = bpy.data.images.get(layer.name, None) if layer_img: layer_width, layer_height = layer_img.size[0], layer_img.size[1] if event.type == 'MOUSEMOVE': region_pos = [event.mouse_region_x, event.mouse_region_y] view_x, view_y = context.region.view2d.region_to_view(*region_pos) target_x = width * view_x target_y = height * view_y layer.location[0] = int(self.start_layer_location[0] \ + target_x - self.start_input_position[0]) layer.location[1] = int(self.start_layer_location[1] \ - (target_y - self.start_input_position[1])) elif event.type == 'LEFTMOUSE': app.rebuild_image_layers_nodes(img) area_session.layer_moving = False area_session.prevent_layer_update_event = False return {'FINISHED'} elif event.type in {'RIGHTMOUSE', 'ESC'}: layer.location = self.start_layer_location area_session.layer_moving = False area_session.prevent_layer_update_event = False return {'CANCELLED'} return {'RUNNING_MODAL'} def invoke(self, context, event): area_session = app.get_area_session(context) img = context.area.spaces.active.image width, height = img.size[0], img.size[1] layer = app.get_active_layer(context) if not layer: return {'CANCELLED'} region_pos = [event.mouse_region_x, event.mouse_region_y] view_x, view_y = context.region.view2d.region_to_view(*region_pos) self.start_input_position = [width * view_x, height * view_y] self.start_layer_location = layer.location[:] area_session.layer_moving = True area_session.prevent_layer_update_event = True context.window_manager.modal_handler_add(self) return {'RUNNING_MODAL'} class IMAGE_EDITOR_PLUS_OT_delete_layer(bpy.types.Operator): bl_idname = "image_editor_plus.delete_layer" bl_label = "Delete Layer" def execute(self, context): img = context.area.spaces.active.image if not img: return {'CANCELLED'} img_props = img.imageeditorplus_properties layers = img_props.layers selected_layer_index = img_props.selected_layer_index if selected_layer_index == -1 or selected_layer_index >= len(layers): return {'CANCELLED'} layer = layers[selected_layer_index] layer_img = bpy.data.images.get(layer.name, None) if layer_img: bpy.data.images.remove(layer_img) layers.remove(selected_layer_index) selected_layer_index = min(max(selected_layer_index, 0), len(layers) - 1) img_props.selected_layer_index = selected_layer_index app.rebuild_image_layers_nodes(img) return {'FINISHED'} class IMAGE_EDITOR_PLUS_OT_change_image_layer_order(bpy.types.Operator): bl_idname = "image_editor_plus.change_image_layer_order" bl_label = "Change Image Layer Order" up: bpy.props.BoolProperty() def execute(self, context): img = context.area.spaces.active.image if not img: return {'CANCELLED'} img_props = img.imageeditorplus_properties layers = img_props.layers selected_layer_index = img_props.selected_layer_index if selected_layer_index == -1 or selected_layer_index >= len(layers): return {'CANCELLED'} if (self.up and selected_layer_index == 0) \ or (not self.up and selected_layer_index >= len(layers) - 1): return {'CANCELLED'} layer = layers[selected_layer_index] new_layer_index = selected_layer_index + (-1 if self.up else 1) layers.move(selected_layer_index, new_layer_index) img_props.selected_layer_index = new_layer_index app.rebuild_image_layers_nodes(img) return {'FINISHED'} class IMAGE_EDITOR_PLUS_OT_merge_layers(bpy.types.Operator): """Merge all layers""" bl_idname = "image_editor_plus.merge_layers" bl_label = "Merge Layers" def execute(self, context): img = context.area.spaces.active.image if not img: return {'CANCELLED'} width, height = img.size pixels = utils.read_pixels_from_image(img) img_props = img.imageeditorplus_properties layers = img_props.layers for layer in reversed(layers): layer_img = bpy.data.images.get(layer.name, None) if not layer_img: continue layer_width, layer_height = layer_img.size[0], layer_img.size[1] layer_pos = layer.location layer_x1, layer_y1 = layer_pos[0], height - layer_height - layer_pos[1] if layer.rotation == 0 and layer.scale[0] == 1.0 and layer.scale[1] == 1.0: layer_pixels = utils.read_pixels_from_image(layer_img) else: layer_pixels, new_layer_width, new_layer_height = \ app.apply_layer_transform(layer_img, layer.rotation, layer.scale) layer_x1 = int(layer_x1 - (new_layer_width - layer_width) / 2.0) layer_y1 = int(layer_y1 - (new_layer_height - layer_height) / 2.0) layer_width = new_layer_width layer_height = new_layer_height layer_x2 = layer_x1 + layer_width layer_y2 = layer_y1 + layer_height # clamp target_x1 = max(min(layer_x1, width), 0) target_y1 = max(min(layer_y1, height), 0) target_x2 = max(min(layer_x2, width), 0) target_y2 = max(min(layer_y2, height), 0) # completely out of the base image if layer_x1 == layer_x2 or layer_y1 == layer_y2: continue src_x1 = target_x1 - layer_x1 src_y1 = target_y1 - layer_y1 src_x2 = layer_width - (layer_x2 - target_x2) src_y2 = layer_height - (layer_y2 - target_y2) target_range = pixels[target_y1:target_y2, target_x1:target_x2] target_color_chan = target_range[:, :, :3] target_alpha_chan = target_range[:, :, 3:4] layer_range = layer_pixels[src_y1:src_y2, src_x1:src_x2] layer_color_chan = layer_range[:, :, :3] layer_alpha_chan = layer_range[:, :, 3:4] # composite temp_alpha_chan = target_alpha_chan * (1.0 - layer_alpha_chan) \ + layer_alpha_chan temp_alpha_chan_safe = \ np.where(temp_alpha_chan == 0, 1.0, temp_alpha_chan) # for div by 0 pixels[target_y1:target_y2, target_x1:target_x2, :3] \ = (target_color_chan * target_alpha_chan * (1.0 - layer_alpha_chan) \ + layer_color_chan * layer_alpha_chan) \ / temp_alpha_chan_safe pixels[target_y1:target_y2, target_x1:target_x2, 3:4] = temp_alpha_chan bpy.data.images.remove(layer_img) utils.write_pixels_to_image(img, pixels) layers.clear() app.rebuild_image_layers_nodes(img) app.refresh_image(context) return {'FINISHED'} class IMAGE_EDITOR_PLUS_OT_flip(bpy.types.Operator): """Flip the image""" bl_idname = "image_editor_plus.flip" bl_label = "Flip" is_vertically: bpy.props.BoolProperty(name="Vertically", default=False) def execute(self, context): wm = context.window_manager img = context.area.spaces.active.image if not img: return {'CANCELLED'} width, height = img.size pixels = utils.read_pixels_from_image(img) if self.is_vertically: new_pixels = np.flipud(pixels) else: new_pixels = np.fliplr(pixels) utils.write_pixels_to_image(img, new_pixels) img_props = img.imageeditorplus_properties layers = img_props.layers for layer in reversed(layers): layer_img = bpy.data.images.get(layer.name, None) if not layer_img: continue layer_width, layer_height = layer_img.size[0], layer_img.size[1] layer_pos = layer.location if self.is_vertically: layer_pos[1] = height - layer_pos[1] - layer_height layer.scale[1] *= -1.0 else: layer_pos[0] = width - layer_pos[0] - layer_width layer.scale[0] *= -1.0 layer.rotation = -layer.rotation app.refresh_image(context) return {'FINISHED'} class IMAGE_EDITOR_PLUS_OT_rotate(bpy.types.Operator): """Rotate the image""" bl_idname = "image_editor_plus.rotate" bl_label = "Rotate" is_left: bpy.props.BoolProperty(name="Left", default=False) def execute(self, context): wm = context.window_manager img = context.area.spaces.active.image if not img: return {'CANCELLED'} width, height = img.size pixels = utils.read_pixels_from_image(img) if self.is_left: new_pixels = np.rot90(pixels, 3) else: new_pixels = np.rot90(pixels) img.scale(height, width) utils.write_pixels_to_image(img, new_pixels) img_props = img.imageeditorplus_properties layers = img_props.layers for layer in reversed(layers): layer_img = bpy.data.images.get(layer.name, None) if not layer_img: continue layer_width, layer_height = layer_img.size[0], layer_img.size[1] layer_pos = layer.location prev_layer_x, prev_layer_y = layer_pos[0], layer_pos[1] if self.is_left: layer_pos[0] = height - prev_layer_y - layer_height \ - (layer_width - layer_height) / 2.0 layer_pos[1] = prev_layer_x + (layer_width - layer_height) / 2.0 layer.rotation += math.pi / 2.0 else: layer_pos[0] = prev_layer_y - (layer_width - layer_height) / 2.0 layer_pos[1] = width - prev_layer_x - layer_width \ + (layer_width - layer_height) / 2.0 layer.rotation -= math.pi / 2.0 app.refresh_image(context) return {'FINISHED'} class IMAGE_EDITOR_PLUS_OT_scale(bpy.types.Operator): """Apply settings""" bl_idname = "image_editor_plus.scale" bl_label = "Scale" width: bpy.props.IntProperty() height: bpy.props.IntProperty() scale_layers: bpy.props.BoolProperty() def execute(self, context): wm = context.window_manager img = context.area.spaces.active.image if not img: return {'CANCELLED'} width, height = img.size img.scale(self.width, self.height) # scaling layers does not work unless the aspect rate is 1:1 if self.scale_layers: scale_x = self.width / width scale_y = self.height / height img_props = img.imageeditorplus_properties layers = img_props.layers for layer in reversed(layers): layer_img = bpy.data.images.get(layer.name, None) if not layer_img: continue layer_width, layer_height = layer_img.size[0], layer_img.size[1] layer_scale = layer.scale layer_scale[0] *= scale_x layer_scale[1] *= scale_y layer_pos = layer.location layer_pos[0] = layer_pos[0] * scale_x - layer_width * (1.0 - scale_x) / 2.0 layer_pos[1] = layer_pos[1] * scale_y - layer_height * (1.0 - scale_y) / 2.0 app.refresh_image(context) return {'FINISHED'} class IMAGE_EDITOR_PLUS_OT_change_canvas_size(bpy.types.Operator): """Apply settings""" bl_idname = "image_editor_plus.change_canvas_size" bl_label = "Change Canvas Size" width: bpy.props.IntProperty() height: bpy.props.IntProperty() expand_from_center: bpy.props.BoolProperty() use_background_color: bpy.props.BoolProperty() def execute(self, context): wm = context.window_manager props = wm.imageeditorplus_properties if self.use_background_color: color = props.background_color[:] + (1.0,) else: color = (0, 0, 0, 0) img = context.area.spaces.active.image if not img: return {'CANCELLED'} width, height = img.size pixels = utils.read_pixels_from_image(img) if self.width > width or self.height > height: expand_width = self.width - width if self.width > width else 0 expand_height = self.height - height if self.height > height else 0 if self.expand_from_center: expand_left = int(expand_width / 2) expand_top = int(expand_height / 2) expand_right = expand_width - expand_left expand_bottom = expand_height - expand_top else: expand_left, expand_top = 0, 0 expand_right = expand_width expand_bottom = expand_height new_pixels = np.pad(pixels, ((expand_bottom, expand_top), (expand_left, expand_right), (0, 0)), constant_values=np.array(((color, color), (color, color), (0, 0)), dtype=object)) else: new_pixels = pixels if self.width < width or self.height < height: shrink_width = width - self.width if self.width < width else 0 shrink_height = height - self.height if self.height < height else 0 if self.expand_from_center: shrink_left = int(shrink_width / 2) shrink_top = int(shrink_height / 2) else: shrink_left, shrink_top = 0, 0 if self.height < height: new_pixels = new_pixels[height - self.height - shrink_top:height - shrink_top, shrink_left:self.width + shrink_left] else: new_pixels = new_pixels[0:self.height, shrink_left:self.width + shrink_left] img.scale(self.width, self.height) utils.write_pixels_to_image(img, new_pixels) app.refresh_image(context) return {'FINISHED'} class IMAGE_EDITOR_PLUS_OT_flip_layer(bpy.types.Operator): """Flip the layer""" bl_idname = "image_editor_plus.flip_layer" bl_label = "Flip Layer" is_vertically: bpy.props.BoolProperty(name="Vertically", default=False) def execute(self, context): img = context.area.spaces.active.image if not img: return {'CANCELLED'} layer = app.get_active_layer(context) if not layer: return {'CANCELLED'} if self.is_vertically: layer.scale[1] *= -1.0 else: layer.scale[0] *= -1.0 app.refresh_image(context) return {'FINISHED'} class IMAGE_EDITOR_PLUS_OT_rotate_layer(bpy.types.Operator): """Rotate the layer""" bl_idname = "image_editor_plus.rotate_layer" bl_label = "Rotate Layer" is_left: bpy.props.BoolProperty(name="Left", default=False) def execute(self, context): img = context.area.spaces.active.image if not img: return {'CANCELLED'} layer = app.get_active_layer(context) if not layer: return {'CANCELLED'} layer.rotation += math.pi / 2.0 if self.is_left else -math.pi / 2.0 app.refresh_image(context) return {'FINISHED'} class IMAGE_EDITOR_PLUS_OT_rotate_layer_arbitrary(bpy.types.Operator): """Rotate the image by a specified angle""" bl_idname = "image_editor_plus.rotate_layer_arbitrary" bl_label = "Rotate Layer Arbitrary" def __init__(self): self.start_input_position = [0, 0] self.start_layer_angle = 0 def modal(self, context, event): area_session = app.get_area_session(context) context.area.tag_redraw() img = context.area.spaces.active.image width, height = img.size[0], img.size[1] layer = app.get_active_layer(context) if not layer: return {'CANCELLED'} layer_width, layer_height = 1, 1 layer_img = bpy.data.images.get(layer.name, None) if layer_img: layer_width, layer_height = layer_img.size[0], layer_img.size[1] if event.type == 'MOUSEMOVE': center_x = layer.location[0] + layer_width / 2.0 center_y = height - layer.location[1] - layer_height / 2.0 region_pos = [event.mouse_region_x, event.mouse_region_y] view_x, view_y = context.region.view2d.region_to_view(*region_pos) target_x = width * view_x target_y = height * view_y angle1 = math.atan2(self.start_input_position[1] - center_y, self.start_input_position[0] - center_x) angle2 = math.atan2(target_y - center_y, target_x - center_x) layer.rotation = self.start_layer_angle + angle2 - angle1 elif event.type == 'LEFTMOUSE': app.rebuild_image_layers_nodes(img) area_session.layer_rotating = False area_session.prevent_layer_update_event = False return {'FINISHED'} elif event.type in {'RIGHTMOUSE', 'ESC'}: layer.rotation = self.start_layer_angle area_session.layer_rotating = False area_session.prevent_layer_update_event = False return {'CANCELLED'} return {'RUNNING_MODAL'} def invoke(self, context, event): area_session = app.get_area_session(context) img = context.area.spaces.active.image width, height = img.size[0], img.size[1] layer = app.get_active_layer(context) if not layer: return {'CANCELLED'} region_pos = [event.mouse_region_x, event.mouse_region_y] view_x, view_y = context.region.view2d.region_to_view(*region_pos) self.start_input_position = [width * view_x, height * view_y] self.start_layer_angle = layer.rotation area_session.layer_rotating = True area_session.prevent_layer_update_event = True context.window_manager.modal_handler_add(self) return {'RUNNING_MODAL'} class IMAGE_EDITOR_PLUS_OT_scale_layer(bpy.types.Operator): """Scale the layer""" bl_idname = "image_editor_plus.scale_layer" bl_label = "Scale Layer" def __init__(self): self.start_input_position = [0, 0] self.start_layer_scale_x = 1.0 self.start_layer_scale_y = 1.0 def modal(self, context, event): area_session = app.get_area_session(context) context.area.tag_redraw() img = context.area.spaces.active.image width, height = img.size[0], img.size[1] layer = app.get_active_layer(context) if not layer: return {'CANCELLED'} layer_width, layer_height = 1, 1 layer_img = bpy.data.images.get(layer.name, None) if layer_img: layer_width, layer_height = layer_img.size[0], layer_img.size[1] if event.type == 'MOUSEMOVE': center_x = layer.location[0] + layer_width / 2.0 center_y = height - layer.location[1] - layer_height / 2.0 region_pos = [event.mouse_region_x, event.mouse_region_y] view_x, view_y = context.region.view2d.region_to_view(*region_pos) target_x = width * view_x target_y = height * view_y dist1 = math.hypot(self.start_input_position[0] - center_x, self.start_input_position[1] - center_y) dist2 = math.hypot(target_x - center_x, target_y - center_y) layer.scale[0] = self.start_layer_scale_x * dist2 / dist1 layer.scale[1] = self.start_layer_scale_y * dist2 / dist1 elif event.type == 'LEFTMOUSE': app.rebuild_image_layers_nodes(img) area_session.layer_scaling = False area_session.prevent_layer_update_event = False return {'FINISHED'} elif event.type in {'RIGHTMOUSE', 'ESC'}: layer.scale[0] = self.start_layer_scale_x layer.scale[1] = self.start_layer_scale_y area_session.layer_scaling = False area_session.prevent_layer_update_event = False return {'CANCELLED'} return {'RUNNING_MODAL'} def invoke(self, context, event): area_session = app.get_area_session(context) img = context.area.spaces.active.image width, height = img.size[0], img.size[1] layer = app.get_active_layer(context) if not layer: return {'CANCELLED'} region_pos = [event.mouse_region_x, event.mouse_region_y] view_x, view_y = context.region.view2d.region_to_view(*region_pos) self.start_input_position = [width * view_x, height * view_y] self.start_layer_scale_x = layer.scale[0] self.start_layer_scale_y = layer.scale[1] area_session.layer_scaling = True area_session.prevent_layer_update_event = True context.window_manager.modal_handler_add(self) return {'RUNNING_MODAL'} class IMAGE_EDITOR_PLUS_OT_offset(bpy.types.Operator): """Apply settings""" bl_idname = 'image_editor_plus.offset' bl_label = "Offset" offset_x: bpy.props.IntProperty() offset_y: bpy.props.IntProperty() offset_edge_behavior: bpy.props.EnumProperty(items=( ('wrap', 'Wrap', 'Wrap image around'), ('edge', 'Edge', 'Repeat edge pixels'))) def execute(self, context): wm = context.window_manager offset_x = self.offset_x offset_y = self.offset_y edge_behavior = self.offset_edge_behavior pixels, hsl = app.get_image_cache() if pixels is None: return {'CANCELLED'} selection = app.get_target_selection(context) if selection: target_pixels = pixels[selection[0][1]:selection[1][1], selection[0][0]:selection[1][0]] elif selection == []: return {'CANCELLED'} else: target_pixels = pixels new_pixels = np.roll(target_pixels, offset_y, axis=0) if edge_behavior == 'edge': if offset_y > 0: new_pixels[:offset_y] = new_pixels[offset_y] elif offset_y < 0: new_pixels[offset_y:] = new_pixels[offset_y - 1] new_pixels = np.roll(new_pixels, offset_x, axis=1) if edge_behavior == 'edge': if offset_x > 0: new_pixels[:, :offset_x] = new_pixels[:, offset_x, np.newaxis] elif offset_x < 0: new_pixels[:, offset_x:] = new_pixels[:, offset_x - 1, np.newaxis] if selection: pixels[selection[0][1]:selection[1][1], selection[0][0]:selection[1][0]] = new_pixels else: pixels[:,:] = new_pixels img = app.get_target_image(context) utils.write_pixels_to_image(img, pixels) app.refresh_image(context) return {'FINISHED'} class IMAGE_EDITOR_PLUS_OT_adjust_color(bpy.types.Operator): """Apply settings""" bl_idname = 'image_editor_plus.adjust_color' bl_label = "Adjust Color" adjust_hue: bpy.props.FloatProperty() adjust_lightness: bpy.props.FloatProperty() adjust_saturation: bpy.props.FloatProperty() def execute(self, context): hue = self.adjust_hue light = self.adjust_lightness sat = self.adjust_saturation pixels, hsl = app.get_image_cache() if pixels is None: return {'CANCELLED'} selection = app.get_target_selection(context) if selection: target_pixels = pixels[selection[0][1]:selection[1][1], selection[0][0]:selection[1][0]] target_hsl = hsl[selection[0][1]:selection[1][1], selection[0][0]:selection[1][0]] elif selection == []: return {'CANCELLED'} else: target_pixels = pixels target_hsl = hsl new_hsl = np.dstack([ (target_hsl[:, :, 0] + hue / 360.0) % 1.0, # hue np.clip(target_hsl[:, :, 1] * sat / 100.0, 0, 1.0), # saturation np.clip(target_hsl[:, :, 2] * light / 100.0, 0, 1.0) # lightness ]) rgb = np.clip(utils.hsl_to_rgb(new_hsl), 0, None) if selection: pixels[selection[0][1]:selection[1][1], selection[0][0]:selection[1][0], 0:3] = rgb else: pixels[:,:,0:3] = rgb img = app.get_target_image(context) utils.write_pixels_to_image(img, pixels) app.refresh_image(context) return {'FINISHED'} class IMAGE_EDITOR_PLUS_OT_adjust_brightness(bpy.types.Operator): """Apply settings""" bl_idname = 'image_editor_plus.adjust_brightness' bl_label = "Adjust Brightness" adjust_brightness: bpy.props.FloatProperty() adjust_contrast: bpy.props.FloatProperty() def execute(self, context): bright = self.adjust_brightness cont = self.adjust_contrast pixels, hsl = app.get_image_cache() if pixels is None: return {'CANCELLED'} selection = app.get_target_selection(context) if selection: target_pixels = pixels[selection[0][1]:selection[1][1], selection[0][0]:selection[1][0]] elif selection == []: return {'CANCELLED'} else: target_pixels = pixels rgb = target_pixels[:, :, :3] cont_fact = cont / 100.0 - 1.0 cont_fact2 = (1.02 * (cont_fact + 1.0)) / (1.0 * (1.02 - cont_fact)) bright_fact = bright / 200.0 - 0.5 new_rgb = (rgb - 0.5) * cont_fact2 + 0.5 + bright_fact new_rgb = np.clip(new_rgb, 0, None) if selection: pixels[selection[0][1]:selection[1][1], selection[0][0]:selection[1][0], 0:3] = new_rgb else: pixels[:,:,0:3] = new_rgb img = app.get_target_image(context) utils.write_pixels_to_image(img, pixels) app.refresh_image(context) return {'FINISHED'} class IMAGE_EDITOR_PLUS_OT_adjust_gamma(bpy.types.Operator): """Apply settings""" bl_idname = 'image_editor_plus.adjust_gamma' bl_label = "Adjust Gamma" adjust_gamma: bpy.props.FloatProperty() def execute(self, context): gamma = self.adjust_gamma pixels, hsl = app.get_image_cache() if pixels is None: return {'CANCELLED'} selection = app.get_target_selection(context) if selection: target_pixels = pixels[selection[0][1]:selection[1][1], selection[0][0]:selection[1][0]] elif selection == []: return {'CANCELLED'} else: target_pixels = pixels rgb = target_pixels[:, :, :3] gamma_inv = 1.0 / gamma new_rgb = rgb ** gamma_inv new_rgb = np.clip(new_rgb, 0, None) if selection: pixels[selection[0][1]:selection[1][1], selection[0][0]:selection[1][0], 0:3] = new_rgb else: pixels[:,:,0:3] = new_rgb img = app.get_target_image(context) utils.write_pixels_to_image(img, pixels) app.refresh_image(context) return {'FINISHED'} class IMAGE_EDITOR_PLUS_OT_adjust_color_curve(bpy.types.Operator): """Apply settings""" bl_idname = 'image_editor_plus.adjust_color_curve' bl_label = "Adjust Color Curve" def execute(self, context): wm = context.window_manager curve_mapping = app.get_curve_mapping() if not curve_mapping: return {'CANCELLED'} curve_mapping.initialize() *curve_rgb, curve_val = curve_mapping.curves pixels, hsl = app.get_image_cache() if pixels is None: return {'CANCELLED'} selection = app.get_target_selection(context) if selection: target_pixels = pixels[selection[0][1]:selection[1][1], selection[0][0]:selection[1][0]] elif selection == []: return {'CANCELLED'} else: target_pixels = pixels rgb = target_pixels[:, :, :3] curve_eval = np.vectorize(curve_mapping.evaluate) new_rgb = curve_eval(curve_val, rgb) new_rgb = np.dstack([ curve_eval(curve_rgb[i], new_rgb[:, :, i]) for i in range(3) ]) new_rgb = np.clip(new_rgb, 0, None) if selection: pixels[selection[0][1]:selection[1][1], selection[0][0]:selection[1][0], 0:3] = new_rgb else: pixels[:,:,0:3] = new_rgb img = app.get_target_image(context) utils.write_pixels_to_image(img, pixels) app.refresh_image(context) return {'FINISHED'} class IMAGE_EDITOR_PLUS_OT_replace_color(bpy.types.Operator): """Apply settings""" bl_idname = 'image_editor_plus.replace_color' bl_label = "Replace Color" source_color: bpy.props.FloatVectorProperty(size=3) replace_color: bpy.props.FloatVectorProperty(size=4) color_threshold: bpy.props.FloatProperty() def execute(self, context): src_color = self.source_color replace_color = self.replace_color threshold = self.color_threshold pixels, hsl = app.get_image_cache() if pixels is None: return {'CANCELLED'} selection = app.get_target_selection(context) if selection: target_pixels = pixels[selection[0][1]:selection[1][1], selection[0][0]:selection[1][0]] elif selection == []: return {'CANCELLED'} else: target_pixels = pixels if threshold <= 0: new_pixels = np.where( np.all(target_pixels[:, :, :3] == src_color, axis=2)[..., np.newaxis], replace_color, target_pixels) else: max_color_dist = np.sqrt(3) * threshold new_pixels = np.where( (np.linalg.norm(target_pixels[:, :, :3] - src_color, axis=2) < max_color_dist)[..., np.newaxis], replace_color, target_pixels) if selection: pixels[selection[0][1]:selection[1][1], selection[0][0]:selection[1][0]] = new_pixels else: pixels[:] = new_pixels img = app.get_target_image(context) utils.write_pixels_to_image(img, pixels) app.refresh_image(context) return {'FINISHED'} class IMAGE_EDITOR_PLUS_OT_blur(bpy.types.Operator): """Apply settings""" bl_idname = 'image_editor_plus.blur' bl_label = "Blur" blur_size: bpy.props.FloatProperty() expand_layer: bpy.props.BoolProperty() def execute(self, context): session = app.get_session() blur_size = self.blur_size need_expand_layer = self.expand_layer pixels, hsl = app.get_image_cache() if pixels is None: return {'CANCELLED'} selection = app.get_target_selection(context) if selection: target_pixels = pixels[selection[0][1]:selection[1][1], selection[0][0]:selection[1][0]] elif selection == []: return {'CANCELLED'} else: target_pixels = pixels expand_size = int(blur_size) layer = app.get_active_layer(context) # expand pixels if layer and need_expand_layer: pixels = np.pad(pixels, ((expand_size, expand_size), (expand_size, expand_size), (0, 0)), mode='constant', constant_values=0) target_pixels = np.pad(target_pixels, ((expand_size, expand_size), (expand_size, expand_size), (0, 0)), mode='constant', constant_values=0) new_pixels = utils.gaussian_blur_core(target_pixels, blur_size) new_pixels = np.clip(new_pixels, 0, None) if selection: pixels[selection[0][1]:selection[1][1], selection[0][0]:selection[1][0]] = new_pixels else: pixels[:] = new_pixels img = app.get_target_image(context) width, height = img.size[0], img.size[1] # resize image layer_resized = False if layer: new_height, new_width, _ = pixels.shape if width != new_width or height != new_height: img.scale(new_width, new_height) cached_layer_location = session.cached_layer_location if need_expand_layer: layer.location[0] = cached_layer_location[0] - expand_size layer.location[1] = cached_layer_location[1] - expand_size else: layer.location[0] = cached_layer_location[0] layer.location[1] = cached_layer_location[1] layer_resized = True utils.write_pixels_to_image(img, pixels) if layer_resized: base_img = context.area.spaces.active.image app.rebuild_image_layers_nodes(base_img) app.refresh_image(context) return {'FINISHED'} class IMAGE_EDITOR_PLUS_OT_sharpen(bpy.types.Operator): """Apply settings""" bl_idname = 'image_editor_plus.sharpen' bl_label = "Sharpen" sharpen_radius: bpy.props.FloatProperty() sharpen_amount: bpy.props.FloatProperty() sharpen_threshold: bpy.props.FloatProperty() def execute(self, context): radius = self.sharpen_radius amount = self.sharpen_amount threshold = self.sharpen_threshold pixels, hsl = app.get_image_cache() if pixels is None: return {'CANCELLED'} selection = app.get_target_selection(context) if selection: target_pixels = pixels[selection[0][1]:selection[1][1], selection[0][0]:selection[1][0]] elif selection == []: return {'CANCELLED'} else: target_pixels = pixels blurred = utils.gaussian_blur_core(target_pixels, radius) new_pixels = (amount + 1.0) * target_pixels - amount * blurred if threshold > 0: low_contrast = np.absolute(target_pixels - blurred) < threshold np.copyto(new_pixels, target_pixels, where=low_contrast) new_pixels = np.clip(new_pixels, 0, None) if selection: pixels[selection[0][1]:selection[1][1], selection[0][0]:selection[1][0]] = new_pixels else: pixels[:] = new_pixels img = app.get_target_image(context) utils.write_pixels_to_image(img, pixels) app.refresh_image(context) return {'FINISHED'} class IMAGE_EDITOR_PLUS_OT_add_noise(bpy.types.Operator): """Apply settings""" bl_idname = 'image_editor_plus.add_noise' bl_label = "Noise" add_noise_intensity: bpy.props.FloatProperty() def execute(self, context): intensity = self.add_noise_intensity sigma = (intensity * 0.02) ** 0.5 pixels, hsl = app.get_image_cache() if pixels is None: return {'CANCELLED'} selection = app.get_target_selection(context) if selection: target_pixels = pixels[selection[0][1]:selection[1][1], selection[0][0]:selection[1][0]] elif selection == []: return {'CANCELLED'} else: target_pixels = pixels rgb = target_pixels[:, :, :3] noise = np.random.normal(0, sigma, (target_pixels.shape[0], target_pixels.shape[1], 3)) new_rgb = rgb + noise new_rgb = np.clip(new_rgb, 0, None) if selection: pixels[selection[0][1]:selection[1][1], selection[0][0]:selection[1][0], 0:3] = new_rgb else: pixels[:,:,0:3] = new_rgb img = app.get_target_image(context) utils.write_pixels_to_image(img, pixels) app.refresh_image(context) return {'FINISHED'} class IMAGE_EDITOR_PLUS_OT_pixelize(bpy.types.Operator): """Apply settings""" bl_idname = 'image_editor_plus.pixelize' bl_label = "Pixelize" pixelize_pixel_size: bpy.props.IntProperty() def execute(self, context): wm = context.window_manager pixel_size = self.pixelize_pixel_size pixels, hsl = app.get_image_cache() if pixels is None: return {'CANCELLED'} selection = app.get_target_selection(context) if selection: target_pixels = pixels[selection[0][1]:selection[1][1], selection[0][0]:selection[1][0]] elif selection == []: return {'CANCELLED'} else: target_pixels = pixels height, width = target_pixels.shape[0], target_pixels.shape[1] pad_x = math.ceil(width / pixel_size) * pixel_size - width pad_y = math.ceil(height / pixel_size) * pixel_size - height target_pixels = np.pad(target_pixels, ((0, pad_y), (0, pad_x), (0, 0)), 'edge') new_pixels = target_pixels[pixel_size // 2::pixel_size, pixel_size // 2::pixel_size] new_pixels = np.repeat(np.repeat(new_pixels, pixel_size, axis=0), pixel_size, axis=1) if selection: pixels[selection[0][1]:selection[1][1], selection[0][0]:selection[1][0]] = new_pixels[:height, :width] else: pixels[:] = new_pixels[:height, :width] img = app.get_target_image(context) utils.write_pixels_to_image(img, pixels) app.refresh_image(context) return {'FINISHED'} class IMAGE_EDITOR_PLUS_OT_make_seamless(bpy.types.Operator): """Apply settings""" bl_idname = 'image_editor_plus.make_seamless' bl_label = "Make Seamless" def execute(self, context): img = app.get_target_image(context) if not img: return {'CANCELLED'} pixels = utils.read_pixels_from_image(img) selection = app.get_target_selection(context) if selection: target_pixels = pixels[selection[0][1]:selection[1][1], selection[0][0]:selection[1][0]] elif selection == []: return {'CANCELLED'} else: target_pixels = pixels target_width, target_height = target_pixels.shape[1], target_pixels.shape[0] target_half_width = target_width // 2 target_half_height = target_height // 2 odd_horz = target_width % 2 odd_vert = target_height % 2 for pat in range(2): if pat == 0: region1_x1, region1_x2 = 0, target_half_width region2_x1, region2_x2 = target_half_width + odd_horz, target_width else: region1_x1, region1_x2 = target_half_width + odd_horz, target_width region2_x1, region2_x2 = 0, target_half_width region1 = target_pixels[0:target_half_height, region1_x1:region1_x2] region2 = target_pixels[target_half_height + odd_vert:target_height, region2_x1:region2_x2] y, x = np.meshgrid(range(target_half_height), range(region1_x1, region1_x2), indexing='ij') a = (np.abs(x - target_half_width) - 1.0) / (target_half_width - 1.0) b = (np.abs(y - target_half_height) - 1.0) / (target_half_height - 1.0) w_denom = a * b + (1.0 - a) * (1.0 - b) w_denom = np.where(w_denom == 0, 1.0, w_denom) # for div by 0 w = 1.0 - a * b / w_denom new_region_chans = [] for i in range(4): new_region_chans.append(w * region1[:,:,i] + (1.0 - w) * region2[:,:,i]) new_region = np.dstack(new_region_chans) region1[:] = new_region region2[:] = new_region if selection: pixels[selection[0][1]:selection[1][1], selection[0][0]:selection[1][0]] = target_pixels else: pixels[:] = target_pixels utils.write_pixels_to_image(img, pixels) app.refresh_image(context) return {'FINISHED'} class IMAGE_EDITOR_PLUS_OT_normal_map(bpy.types.Operator): """Apply settings""" bl_idname = 'image_editor_plus.normal_map' bl_label = "Normal Map" scale: bpy.props.FloatProperty() flip_x: bpy.props.BoolProperty() flip_y: bpy.props.BoolProperty() full_z: bpy.props.BoolProperty() def execute(self, context): scale = self.scale flip_x = self.flip_x flip_y = self.flip_y full_z = self.full_z pixels, hsl = app.get_image_cache() if pixels is None: return {'CANCELLED'} selection = app.get_target_selection(context) if selection: target_hsl = hsl[selection[0][1]:selection[1][1], selection[0][0]:selection[1][0]] elif selection == []: return {'CANCELLED'} else: target_hsl = hsl target_width, target_height = target_hsl.shape[1], target_hsl.shape[0] target_hsl = np.pad(target_hsl, ((1, 1), (1, 1), (0, 0)), 'edge') new_pixels = np.zeros((target_height, target_width, 4)) nx = (target_hsl[1:target_height + 1, 0:target_width, 2] - target_hsl[1:target_height + 1, 2:target_width + 2, 2]) * scale ny = (target_hsl[0:target_height, 1:target_width + 1, 2] - target_hsl[2:target_height + 2, 1:target_width + 1, 2]) * scale nz = 1.0 / np.sqrt(nx * nx + ny * ny + 1.0) nx *= nz ny *= nz new_pixels = np.dstack([ 0.5 + (-0.5 if flip_x else 0.5) * nx, 0.5 + (-0.5 if flip_y else 0.5) * ny, (0 if full_z else 0.5) + (1.0 if full_z else 0.5) * nz, ]) new_pixels = np.clip(new_pixels, 0, None) if selection: pixels[selection[0][1]:selection[1][1], selection[0][0]:selection[1][0], 0:3] = new_pixels else: pixels[:,:,0:3] = new_pixels img = app.get_target_image(context) utils.write_pixels_to_image(img, pixels) app.refresh_image(context) return {'FINISHED'}