bl-image-editor-plus/addon/operators.py

1565 lines
49 KiB
Python

'''
Copyright (C) 2021 - 2023 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 <http://www.gnu.org/licenses/>.
'''
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_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'}