1709 lines
54 KiB
Python
1709 lines
54 KiB
Python
'''
|
|
Copyright (C) 2021 - 2025 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, *args, **kwargs):
|
|
super().__init__(*args, **kwargs)
|
|
|
|
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
|
|
|
|
elif event.type == 'LEFTMOUSE':
|
|
if event.value == 'PRESS':
|
|
self.lmb = True
|
|
|
|
region_pos = [event.mouse_region_x, event.mouse_region_y]
|
|
area_session.selection_region = [region_pos, region_pos]
|
|
|
|
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, *args, **kwargs):
|
|
super().__init__(*args, **kwargs)
|
|
|
|
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, *args, **kwargs):
|
|
super().__init__(*args, **kwargs)
|
|
|
|
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, *args, **kwargs):
|
|
super().__init__(*args, **kwargs)
|
|
|
|
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'}
|