Initial commit
|
@ -0,0 +1,2 @@
|
||||||
|
/build*/
|
||||||
|
__pycache__/
|
|
@ -0,0 +1,205 @@
|
||||||
|
'''
|
||||||
|
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/>.
|
||||||
|
'''
|
||||||
|
|
||||||
|
bl_info = {
|
||||||
|
"name": "Image Editor Plus",
|
||||||
|
"author": "akaneyu",
|
||||||
|
"version": (1, 7, 3),
|
||||||
|
"blender": (2, 93, 0),
|
||||||
|
"location": "Image",
|
||||||
|
"warning": "",
|
||||||
|
"description": "",
|
||||||
|
"wiki_url": "",
|
||||||
|
"tracker_url": "",
|
||||||
|
"category": "Paint"}
|
||||||
|
|
||||||
|
if "bpy" in locals():
|
||||||
|
import importlib
|
||||||
|
importlib.reload(app)
|
||||||
|
importlib.reload(api)
|
||||||
|
importlib.reload(operators)
|
||||||
|
importlib.reload(ui)
|
||||||
|
importlib.reload(ui_renderer)
|
||||||
|
importlib.reload(utils)
|
||||||
|
|
||||||
|
import bpy
|
||||||
|
from . import app
|
||||||
|
from . import api
|
||||||
|
from . import operators
|
||||||
|
from . import ui
|
||||||
|
|
||||||
|
classes = [
|
||||||
|
app.IMAGE_EDITOR_PLUS_WindowPropertyGroup,
|
||||||
|
app.IMAGE_EDITOR_PLUS_LayerPropertyGroup,
|
||||||
|
app.IMAGE_EDITOR_PLUS_ImagePropertyGroup,
|
||||||
|
operators.IMAGE_EDITOR_PLUS_OT_make_selection,
|
||||||
|
operators.IMAGE_EDITOR_PLUS_OT_cancel_selection,
|
||||||
|
operators.IMAGE_EDITOR_PLUS_OT_swap_colors,
|
||||||
|
operators.IMAGE_EDITOR_PLUS_OT_fill_with_fg_color,
|
||||||
|
operators.IMAGE_EDITOR_PLUS_OT_fill_with_bg_color,
|
||||||
|
operators.IMAGE_EDITOR_PLUS_OT_clear,
|
||||||
|
operators.IMAGE_EDITOR_PLUS_OT_cut,
|
||||||
|
operators.IMAGE_EDITOR_PLUS_OT_copy,
|
||||||
|
operators.IMAGE_EDITOR_PLUS_OT_paste,
|
||||||
|
operators.IMAGE_EDITOR_PLUS_OT_crop,
|
||||||
|
operators.IMAGE_EDITOR_PLUS_OT_deselect_layer,
|
||||||
|
operators.IMAGE_EDITOR_PLUS_OT_move_layer,
|
||||||
|
operators.IMAGE_EDITOR_PLUS_OT_delete_layer,
|
||||||
|
operators.IMAGE_EDITOR_PLUS_OT_change_image_layer_order,
|
||||||
|
operators.IMAGE_EDITOR_PLUS_OT_merge_layers,
|
||||||
|
operators.IMAGE_EDITOR_PLUS_OT_flip,
|
||||||
|
operators.IMAGE_EDITOR_PLUS_OT_rotate,
|
||||||
|
operators.IMAGE_EDITOR_PLUS_OT_scale,
|
||||||
|
operators.IMAGE_EDITOR_PLUS_OT_flip_layer,
|
||||||
|
operators.IMAGE_EDITOR_PLUS_OT_rotate_layer,
|
||||||
|
operators.IMAGE_EDITOR_PLUS_OT_rotate_layer_arbitrary,
|
||||||
|
operators.IMAGE_EDITOR_PLUS_OT_scale_layer,
|
||||||
|
operators.IMAGE_EDITOR_PLUS_OT_offset,
|
||||||
|
operators.IMAGE_EDITOR_PLUS_OT_adjust_color,
|
||||||
|
operators.IMAGE_EDITOR_PLUS_OT_adjust_brightness,
|
||||||
|
operators.IMAGE_EDITOR_PLUS_OT_adjust_gamma,
|
||||||
|
operators.IMAGE_EDITOR_PLUS_OT_adjust_color_curve,
|
||||||
|
operators.IMAGE_EDITOR_PLUS_OT_replace_color,
|
||||||
|
operators.IMAGE_EDITOR_PLUS_OT_blur,
|
||||||
|
operators.IMAGE_EDITOR_PLUS_OT_sharpen,
|
||||||
|
operators.IMAGE_EDITOR_PLUS_OT_add_noise,
|
||||||
|
operators.IMAGE_EDITOR_PLUS_OT_pixelize,
|
||||||
|
operators.IMAGE_EDITOR_PLUS_OT_make_seamless,
|
||||||
|
ui.IMAGE_EDITOR_PLUS_OT_scale_dialog,
|
||||||
|
ui.IMAGE_EDITOR_PLUS_OffsetPropertyGroup,
|
||||||
|
ui.IMAGE_EDITOR_PLUS_OT_offset_dialog,
|
||||||
|
ui.IMAGE_EDITOR_PLUS_OT_adjust_color_dialog,
|
||||||
|
ui.IMAGE_EDITOR_PLUS_OT_adjust_brightness_dialog,
|
||||||
|
ui.IMAGE_EDITOR_PLUS_OT_adjust_gamma_dialog,
|
||||||
|
ui.IMAGE_EDITOR_PLUS_OT_adjust_color_curve_dialog,
|
||||||
|
ui.IMAGE_EDITOR_PLUS_OT_replace_color_dialog,
|
||||||
|
ui.IMAGE_EDITOR_PLUS_OT_blur_dialog,
|
||||||
|
ui.IMAGE_EDITOR_PLUS_OT_sharpen_dialog,
|
||||||
|
ui.IMAGE_EDITOR_PLUS_OT_add_noise_dialog,
|
||||||
|
ui.IMAGE_EDITOR_PLUS_OT_pixelize_dialog,
|
||||||
|
ui.IMAGE_EDITOR_PLUS_OT_make_seamless_dialog,
|
||||||
|
ui.IMAGE_EDITOR_PLUS_UL_layer_list,
|
||||||
|
ui.IMAGE_EDITOR_PLUS_MT_edit_menu,
|
||||||
|
ui.IMAGE_EDITOR_PLUS_MT_layers_menu,
|
||||||
|
ui.IMAGE_EDITOR_PLUS_MT_transform_menu,
|
||||||
|
ui.IMAGE_EDITOR_PLUS_MT_transform_layer_menu,
|
||||||
|
ui.IMAGE_EDITOR_PLUS_MT_offset_menu,
|
||||||
|
ui.IMAGE_EDITOR_PLUS_MT_adjust_menu,
|
||||||
|
ui.IMAGE_EDITOR_PLUS_MT_filter_menu,
|
||||||
|
ui.IMAGE_EDITOR_PLUS_PT_select_panel,
|
||||||
|
ui.IMAGE_EDITOR_PLUS_PT_edit_panel,
|
||||||
|
ui.IMAGE_EDITOR_PLUS_PT_layers_panel,
|
||||||
|
ui.IMAGE_EDITOR_PLUS_PT_transform_panel,
|
||||||
|
ui.IMAGE_EDITOR_PLUS_PT_transform_layer_panel,
|
||||||
|
ui.IMAGE_EDITOR_PLUS_PT_offset_panel,
|
||||||
|
ui.IMAGE_EDITOR_PLUS_PT_adjust_panel,
|
||||||
|
ui.IMAGE_EDITOR_PLUS_PT_filter_panel
|
||||||
|
]
|
||||||
|
|
||||||
|
def register_keymaps():
|
||||||
|
session = app.get_session()
|
||||||
|
|
||||||
|
keymaps = session.keymaps
|
||||||
|
|
||||||
|
wm = bpy.context.window_manager
|
||||||
|
|
||||||
|
km = wm.keyconfigs.addon.keymaps.new(name='Image Generic', space_type='IMAGE_EDITOR')
|
||||||
|
kmi = km.keymap_items.new(operators.IMAGE_EDITOR_PLUS_OT_make_selection.bl_idname,
|
||||||
|
'B', 'PRESS')
|
||||||
|
kmi = km.keymap_items.new(operators.IMAGE_EDITOR_PLUS_OT_cancel_selection.bl_idname,
|
||||||
|
'D', 'PRESS', ctrl=True)
|
||||||
|
kmi = km.keymap_items.new(operators.IMAGE_EDITOR_PLUS_OT_cut.bl_idname,
|
||||||
|
'X', 'PRESS', ctrl=True)
|
||||||
|
kmi = km.keymap_items.new(operators.IMAGE_EDITOR_PLUS_OT_copy.bl_idname,
|
||||||
|
'C', 'PRESS', ctrl=True)
|
||||||
|
kmi = km.keymap_items.new(operators.IMAGE_EDITOR_PLUS_OT_paste.bl_idname,
|
||||||
|
'V', 'PRESS', ctrl=True)
|
||||||
|
kmi = km.keymap_items.new(operators.IMAGE_EDITOR_PLUS_OT_fill_with_fg_color.bl_idname,
|
||||||
|
'DEL', 'PRESS', ctrl=True)
|
||||||
|
kmi = km.keymap_items.new(operators.IMAGE_EDITOR_PLUS_OT_clear.bl_idname,
|
||||||
|
'DEL', 'PRESS')
|
||||||
|
kmi = km.keymap_items.new(operators.IMAGE_EDITOR_PLUS_OT_move_layer.bl_idname,
|
||||||
|
'G', 'PRESS')
|
||||||
|
kmi = km.keymap_items.new(operators.IMAGE_EDITOR_PLUS_OT_rotate_layer_arbitrary.bl_idname,
|
||||||
|
'R', 'PRESS')
|
||||||
|
kmi = km.keymap_items.new(operators.IMAGE_EDITOR_PLUS_OT_scale_layer.bl_idname,
|
||||||
|
'C', 'PRESS')
|
||||||
|
|
||||||
|
keymaps.append(km)
|
||||||
|
|
||||||
|
def unregister_keymaps():
|
||||||
|
session = app.get_session()
|
||||||
|
|
||||||
|
wm = bpy.context.window_manager
|
||||||
|
|
||||||
|
for km in session.keymaps:
|
||||||
|
wm.keyconfigs.addon.keymaps.remove(km)
|
||||||
|
|
||||||
|
def register():
|
||||||
|
global draw_handler
|
||||||
|
|
||||||
|
api.API.VERSION = bl_info['version']
|
||||||
|
|
||||||
|
app.load_icons()
|
||||||
|
|
||||||
|
for cls in classes:
|
||||||
|
bpy.utils.register_class(cls)
|
||||||
|
|
||||||
|
bpy.types.IMAGE_MT_image.append(ui.menu_func)
|
||||||
|
|
||||||
|
bpy.app.handlers.save_pre.append(app.save_pre_handler)
|
||||||
|
|
||||||
|
draw_handler = bpy.types.SpaceImageEditor.draw_handler_add(
|
||||||
|
app.draw_handler, (), 'WINDOW', 'POST_PIXEL')
|
||||||
|
|
||||||
|
wm = bpy.types.WindowManager
|
||||||
|
|
||||||
|
wm.imageeditorplus_api = api.API
|
||||||
|
wm.imageeditorplus_properties = \
|
||||||
|
bpy.props.PointerProperty(type=app.IMAGE_EDITOR_PLUS_WindowPropertyGroup)
|
||||||
|
bpy.types.Image.imageeditorplus_properties = \
|
||||||
|
bpy.props.PointerProperty(type=app.IMAGE_EDITOR_PLUS_ImagePropertyGroup)
|
||||||
|
|
||||||
|
register_keymaps()
|
||||||
|
|
||||||
|
def unregister():
|
||||||
|
global draw_handler
|
||||||
|
|
||||||
|
app.cleanup_scene()
|
||||||
|
|
||||||
|
app.dispose_icons()
|
||||||
|
|
||||||
|
for cls in classes:
|
||||||
|
bpy.utils.unregister_class(cls)
|
||||||
|
|
||||||
|
bpy.types.IMAGE_MT_image.remove(ui.menu_func)
|
||||||
|
|
||||||
|
bpy.app.handlers.save_pre.remove(app.save_pre_handler)
|
||||||
|
|
||||||
|
bpy.types.SpaceImageEditor.draw_handler_remove(draw_handler, 'WINDOW')
|
||||||
|
|
||||||
|
wm = bpy.types.WindowManager
|
||||||
|
|
||||||
|
del wm.imageeditorplus_properties
|
||||||
|
del bpy.types.Image.imageeditorplus_properties
|
||||||
|
|
||||||
|
unregister_keymaps()
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
register()
|
||||||
|
|
||||||
|
draw_handler = None
|
|
@ -0,0 +1,103 @@
|
||||||
|
'''
|
||||||
|
Copyright (C) 2021 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 bpy
|
||||||
|
from . import app
|
||||||
|
from . import utils
|
||||||
|
|
||||||
|
class API:
|
||||||
|
VERSION = (0, 0, 0) # set by register()
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def read_pixels_from_image(img):
|
||||||
|
return utils.read_pixels_from_image(img)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def write_pixels_to_image(img, pixels):
|
||||||
|
utils.write_pixels_to_image(img, pixels)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def refresh_image(context):
|
||||||
|
app.refresh_image(context)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def select_layer(img, layer):
|
||||||
|
img_props = img.imageeditorplus_properties
|
||||||
|
layers = img_props.layers
|
||||||
|
|
||||||
|
if layer and layer in layers:
|
||||||
|
img_props.selected_layer_index = layers.index(layer)
|
||||||
|
else:
|
||||||
|
img_props.selected_layer_index = -1
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def get_selected_layer(img):
|
||||||
|
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 None
|
||||||
|
|
||||||
|
return layers[selected_layer_index]
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def create_layer(base_img, pixels, img_settings={}, layer_settings={}):
|
||||||
|
img_settings_mod = {
|
||||||
|
'is_float': img_settings.get('is_float', True),
|
||||||
|
'colorspace_name': img_settings.get('colorspace_name', 'Linear')
|
||||||
|
}
|
||||||
|
|
||||||
|
layer_settings_mod = {
|
||||||
|
'rotation': layer_settings.get('rotation', 0),
|
||||||
|
'scale': layer_settings.get('scale', [1.0, 1.0]),
|
||||||
|
'custom_data': layer_settings.get('custom_data', '{}')
|
||||||
|
}
|
||||||
|
|
||||||
|
app.create_layer(base_img, pixels, img_settings_mod, layer_settings_mod)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def read_pixels_from_layer(layer):
|
||||||
|
layer_img = bpy.data.images.get(layer.name, None)
|
||||||
|
if not layer_img:
|
||||||
|
return 0, 0, None
|
||||||
|
|
||||||
|
layer_width, layer_height = layer_img.size[0], layer_img.size[1]
|
||||||
|
|
||||||
|
layer_pixels = utils.read_pixels_from_image(layer_img)
|
||||||
|
|
||||||
|
return layer_width, layer_height, layer_pixels
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def write_pixels_to_layer(layer, pixels):
|
||||||
|
layer_img = bpy.data.images.get(layer.name, None)
|
||||||
|
if not layer_img:
|
||||||
|
return
|
||||||
|
|
||||||
|
utils.write_pixels_to_image(layer_img, pixels)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def scale_layer(layer, width, height):
|
||||||
|
layer_img = bpy.data.images.get(layer.name, None)
|
||||||
|
if not layer_img:
|
||||||
|
return
|
||||||
|
|
||||||
|
layer_img.scale(width, height)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def update_layers(img):
|
||||||
|
app.rebuild_image_layers_nodes(img)
|
|
@ -0,0 +1,531 @@
|
||||||
|
'''
|
||||||
|
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 os
|
||||||
|
import bpy
|
||||||
|
import bpy.utils.previews
|
||||||
|
import blf
|
||||||
|
import numpy as np
|
||||||
|
from . ui_renderer import UIRenderer as UIRenderer
|
||||||
|
from . import utils
|
||||||
|
|
||||||
|
class AreaSession:
|
||||||
|
def __init__(self):
|
||||||
|
self.selection = None
|
||||||
|
self.selection_region = None
|
||||||
|
self.selecting = False
|
||||||
|
self.layer_moving = False
|
||||||
|
self.layer_rotating = False
|
||||||
|
self.layer_scaling = False
|
||||||
|
self.prevent_layer_update_event = False
|
||||||
|
self.prev_image = None
|
||||||
|
|
||||||
|
class Session:
|
||||||
|
def __init__(self):
|
||||||
|
self.icons = None
|
||||||
|
self.keymaps = []
|
||||||
|
self.cached_image_pixels = None
|
||||||
|
self.cached_image_hsl = None
|
||||||
|
self.cached_layer_location = None
|
||||||
|
self.ui_renderer = None
|
||||||
|
self.copied_image_pixels = None
|
||||||
|
self.copied_image_settings = None
|
||||||
|
self.copied_layer_settings = None
|
||||||
|
self.areas = {}
|
||||||
|
|
||||||
|
def get_session():
|
||||||
|
global session
|
||||||
|
|
||||||
|
return session
|
||||||
|
|
||||||
|
def get_area_session(context):
|
||||||
|
area_session = session.areas.get(context.area, None)
|
||||||
|
if not area_session:
|
||||||
|
area_session = AreaSession()
|
||||||
|
session.areas[context.area] = area_session
|
||||||
|
|
||||||
|
return area_session
|
||||||
|
|
||||||
|
def draw_handler():
|
||||||
|
global session
|
||||||
|
|
||||||
|
context = bpy.context
|
||||||
|
area_session = get_area_session(context)
|
||||||
|
|
||||||
|
info_text = None
|
||||||
|
|
||||||
|
width, height, img_name = 0, 0, ''
|
||||||
|
img = context.area.spaces.active.image
|
||||||
|
if img:
|
||||||
|
width, height = img.size[0], img.size[1]
|
||||||
|
|
||||||
|
# render selection frame
|
||||||
|
if img and area_session.selection or area_session.selection_region:
|
||||||
|
if area_session.selection:
|
||||||
|
selection = area_session.selection
|
||||||
|
view_x1 = selection[0][0] / width
|
||||||
|
view_y1 = selection[0][1] / height
|
||||||
|
view_x2 = selection[1][0] / width
|
||||||
|
view_y2 = selection[1][1] / height
|
||||||
|
|
||||||
|
region_pos1 = context.region.view2d.view_to_region(view_x1, view_y1, clip=False)
|
||||||
|
region_pos2 = context.region.view2d.view_to_region(view_x2, view_y2, clip=False)
|
||||||
|
else:
|
||||||
|
region_pos1, region_pos2 = area_session.selection_region
|
||||||
|
|
||||||
|
if not session.ui_renderer:
|
||||||
|
session.ui_renderer = UIRenderer()
|
||||||
|
|
||||||
|
region_size = [region_pos2[0] - region_pos1[0],
|
||||||
|
region_pos2[1] - region_pos1[1]]
|
||||||
|
|
||||||
|
session.ui_renderer.render_selection_frame(region_pos1, region_size)
|
||||||
|
|
||||||
|
# render layers
|
||||||
|
if img:
|
||||||
|
if not session.ui_renderer:
|
||||||
|
session.ui_renderer = UIRenderer()
|
||||||
|
|
||||||
|
img_props = img.imageeditorplus_properties
|
||||||
|
selected_layer_index = img_props.selected_layer_index
|
||||||
|
layers = img_props.layers
|
||||||
|
|
||||||
|
for i, layer in reversed(list(enumerate(layers))):
|
||||||
|
layer_img = bpy.data.images.get(layer.name, None)
|
||||||
|
if layer_img:
|
||||||
|
layer_width, layer_height = layer_img.size[0], layer_img.size[1]
|
||||||
|
layer_pos = layer.location
|
||||||
|
layer_pos1 = [layer_pos[0], layer_pos[1] + layer_height]
|
||||||
|
layer_pos2 = [layer_pos[0] + layer_width, layer_pos[1]]
|
||||||
|
|
||||||
|
layer_view_x1 = layer_pos1[0] / width
|
||||||
|
layer_view_y1 = 1.0 - layer_pos1[1] / height
|
||||||
|
layer_region_pos1 = context.region.view2d.view_to_region(
|
||||||
|
layer_view_x1, layer_view_y1, clip=False)
|
||||||
|
|
||||||
|
layer_view_x2 = layer_pos2[0] / width
|
||||||
|
layer_view_y2 = 1.0 - layer_pos2[1] / height
|
||||||
|
layer_region_pos2 = context.region.view2d.view_to_region(
|
||||||
|
layer_view_x2, layer_view_y2, clip=False)
|
||||||
|
|
||||||
|
layer_region_size = [layer_region_pos2[0] - layer_region_pos1[0],
|
||||||
|
layer_region_pos2[1] - layer_region_pos1[1]]
|
||||||
|
|
||||||
|
if not layer.hide:
|
||||||
|
session.ui_renderer.render_image(layer_img, layer_region_pos1,
|
||||||
|
layer_region_size, layer.rotation, layer.scale)
|
||||||
|
|
||||||
|
if i == selected_layer_index:
|
||||||
|
session.ui_renderer.render_selection_frame(
|
||||||
|
layer_region_pos1, layer_region_size, layer.rotation,
|
||||||
|
layer.scale)
|
||||||
|
|
||||||
|
# release the selection if the image is changed
|
||||||
|
if area_session.selection or area_session.selection_region:
|
||||||
|
if img != area_session.prev_image:
|
||||||
|
cancel_selection(context)
|
||||||
|
|
||||||
|
elif width != area_session.prev_image_width \
|
||||||
|
or height != area_session.prev_image_height:
|
||||||
|
|
||||||
|
crop_selection(context)
|
||||||
|
|
||||||
|
area_session.prev_image = img
|
||||||
|
area_session.prev_image_width = width
|
||||||
|
area_session.prev_image_height = height
|
||||||
|
|
||||||
|
if area_session.layer_moving \
|
||||||
|
or area_session.layer_rotating \
|
||||||
|
or area_session.layer_scaling:
|
||||||
|
|
||||||
|
info_text = "LMB: Perform\n" \
|
||||||
|
+ "RMB: Cancel"
|
||||||
|
|
||||||
|
area_height = context.area.height
|
||||||
|
|
||||||
|
# info text
|
||||||
|
if info_text:
|
||||||
|
blf.enable(0, blf.WORD_WRAP)
|
||||||
|
blf.word_wrap(0, 100)
|
||||||
|
|
||||||
|
blf.position(0, 30, area_height - 70, 0)
|
||||||
|
blf.size(0, 14, 72)
|
||||||
|
blf.draw(0, info_text)
|
||||||
|
|
||||||
|
blf.disable(0, blf.WORD_WRAP)
|
||||||
|
|
||||||
|
def get_curve_node():
|
||||||
|
node_group = bpy.data.node_groups.get('imageeditorplus')
|
||||||
|
if not node_group:
|
||||||
|
node_group = bpy.data.node_groups.new('imageeditorplus', 'ShaderNodeTree')
|
||||||
|
|
||||||
|
nodes = node_group.nodes
|
||||||
|
|
||||||
|
curve_node = next((node for node in nodes if node.bl_idname == 'ShaderNodeRGBCurve'),
|
||||||
|
None)
|
||||||
|
if not curve_node:
|
||||||
|
curve_node = nodes.new('ShaderNodeRGBCurve')
|
||||||
|
|
||||||
|
return curve_node
|
||||||
|
|
||||||
|
def get_curve_mapping():
|
||||||
|
return get_curve_node().mapping
|
||||||
|
|
||||||
|
def reset_curve_mapping():
|
||||||
|
curve_mapping = get_curve_mapping()
|
||||||
|
|
||||||
|
for curve in curve_mapping.curves:
|
||||||
|
curve_points = curve.points
|
||||||
|
num_curve_points = len(curve_points)
|
||||||
|
|
||||||
|
# remove extra points (> 2)
|
||||||
|
if num_curve_points > 2:
|
||||||
|
for i in range(num_curve_points - 2):
|
||||||
|
curve_points.remove(curve_points[2])
|
||||||
|
|
||||||
|
curve_points[0].location[0] = 0
|
||||||
|
curve_points[0].location[1] = 0
|
||||||
|
curve_points[0].select = False
|
||||||
|
curve_points[1].location[0] = 1.0
|
||||||
|
curve_points[1].location[1] = 1.0
|
||||||
|
curve_points[1].select = False
|
||||||
|
|
||||||
|
curve_mapping.update()
|
||||||
|
|
||||||
|
def get_active_layer(context):
|
||||||
|
img = context.area.spaces.active.image
|
||||||
|
if not img:
|
||||||
|
return None
|
||||||
|
|
||||||
|
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 None
|
||||||
|
|
||||||
|
return layers[selected_layer_index]
|
||||||
|
|
||||||
|
def get_target_image(context):
|
||||||
|
layer = get_active_layer(context)
|
||||||
|
if layer:
|
||||||
|
return bpy.data.images.get(layer.name, None)
|
||||||
|
else:
|
||||||
|
return context.area.spaces.active.image
|
||||||
|
|
||||||
|
def cache_image(img, need_hsl=False):
|
||||||
|
global session
|
||||||
|
|
||||||
|
pixels = utils.read_pixels_from_image(img)
|
||||||
|
|
||||||
|
session.cached_image_pixels = pixels
|
||||||
|
|
||||||
|
hsl = None
|
||||||
|
if need_hsl:
|
||||||
|
session.cached_image_hsl = utils.rgb_to_hsl(pixels)
|
||||||
|
|
||||||
|
return pixels, hsl
|
||||||
|
|
||||||
|
def get_image_cache():
|
||||||
|
global session
|
||||||
|
|
||||||
|
pixels = session.cached_image_pixels
|
||||||
|
hsl = session.cached_image_hsl
|
||||||
|
|
||||||
|
if pixels is not None:
|
||||||
|
pixels = pixels.copy()
|
||||||
|
if hsl is not None:
|
||||||
|
hsl = hsl.copy()
|
||||||
|
|
||||||
|
return pixels, hsl
|
||||||
|
|
||||||
|
def revert_image_cache(img):
|
||||||
|
global session
|
||||||
|
|
||||||
|
pixels = session.cached_image_pixels
|
||||||
|
if pixels is None:
|
||||||
|
return
|
||||||
|
|
||||||
|
utils.write_pixels_to_image(img, pixels)
|
||||||
|
|
||||||
|
def clear_image_cache():
|
||||||
|
global session
|
||||||
|
|
||||||
|
session.cached_image_pixels = None
|
||||||
|
session.cached_image_hsl = None
|
||||||
|
|
||||||
|
def convert_selection(context):
|
||||||
|
area_session = get_area_session(context)
|
||||||
|
|
||||||
|
img = context.area.spaces.active.image
|
||||||
|
if not img:
|
||||||
|
return
|
||||||
|
|
||||||
|
width, height = img.size[0], img.size[1]
|
||||||
|
|
||||||
|
selection_region = area_session.selection_region
|
||||||
|
if not selection_region:
|
||||||
|
return
|
||||||
|
|
||||||
|
x1, y1 = context.region.view2d.region_to_view(*selection_region[0])
|
||||||
|
x2, y2 = context.region.view2d.region_to_view(*selection_region[1])
|
||||||
|
|
||||||
|
x1, x2 = sorted((x1, x2))
|
||||||
|
y1, y2 = sorted((y1, y2))
|
||||||
|
|
||||||
|
x1 = round(x1 * width)
|
||||||
|
y1 = round(y1 * height)
|
||||||
|
x2 = round(x2 * width)
|
||||||
|
y2 = round(y2 * height)
|
||||||
|
|
||||||
|
area_session.selection = [[x1, y1], [x2, y2]]
|
||||||
|
|
||||||
|
crop_selection(context)
|
||||||
|
|
||||||
|
def crop_selection(context):
|
||||||
|
area_session = get_area_session(context)
|
||||||
|
|
||||||
|
img = context.area.spaces.active.image
|
||||||
|
if not img:
|
||||||
|
return
|
||||||
|
|
||||||
|
width, height = img.size[0], img.size[1]
|
||||||
|
|
||||||
|
if not area_session.selection:
|
||||||
|
return
|
||||||
|
|
||||||
|
[x1, y1], [x2, y2] = area_session.selection
|
||||||
|
|
||||||
|
# clamp
|
||||||
|
x1 = max(min(x1, width), 0)
|
||||||
|
y1 = max(min(y1, height), 0)
|
||||||
|
x2 = max(min(x2, width), 0)
|
||||||
|
y2 = max(min(y2, height), 0)
|
||||||
|
|
||||||
|
# avoid from zero width or height
|
||||||
|
if x2 - x1 <= 0:
|
||||||
|
if x2 < width:
|
||||||
|
x2 = x2 + 1
|
||||||
|
else:
|
||||||
|
x1 = x1 - 1
|
||||||
|
|
||||||
|
if y2 - y1 <= 0:
|
||||||
|
if y2 < height:
|
||||||
|
y2 = y2 + 1
|
||||||
|
else:
|
||||||
|
y1 = y1 - 1
|
||||||
|
|
||||||
|
area_session.selection = [[x1, y1], [x2, y2]]
|
||||||
|
|
||||||
|
def cancel_selection(context):
|
||||||
|
global session
|
||||||
|
|
||||||
|
area_session = get_area_session(context)
|
||||||
|
|
||||||
|
area_session.selection = None
|
||||||
|
area_session.selection_region = None
|
||||||
|
|
||||||
|
def get_selection(context):
|
||||||
|
global session
|
||||||
|
|
||||||
|
area_session = get_area_session(context)
|
||||||
|
|
||||||
|
return area_session.selection
|
||||||
|
|
||||||
|
def get_target_selection(context):
|
||||||
|
global session
|
||||||
|
|
||||||
|
area_session = get_area_session(context)
|
||||||
|
|
||||||
|
selection = area_session.selection
|
||||||
|
if not selection:
|
||||||
|
return None
|
||||||
|
|
||||||
|
img = context.area.spaces.active.image
|
||||||
|
if not img:
|
||||||
|
return selection
|
||||||
|
|
||||||
|
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 selection
|
||||||
|
|
||||||
|
return None
|
||||||
|
|
||||||
|
def refresh_image(context):
|
||||||
|
wm = context.window_manager
|
||||||
|
|
||||||
|
img = context.area.spaces.active.image
|
||||||
|
if not img:
|
||||||
|
return
|
||||||
|
|
||||||
|
context.area.spaces.active.image = img
|
||||||
|
|
||||||
|
img.update()
|
||||||
|
|
||||||
|
if not hasattr(wm, 'imagelayersnode_api') or wm.imagelayersnode_api.VERSION < (1, 1, 0):
|
||||||
|
return
|
||||||
|
|
||||||
|
wm.imagelayersnode_api.update_pasted_layer_nodes(img)
|
||||||
|
|
||||||
|
def apply_layer_transform(img, rot, scale):
|
||||||
|
global session
|
||||||
|
|
||||||
|
if not session.ui_renderer:
|
||||||
|
session.ui_renderer = UIRenderer()
|
||||||
|
|
||||||
|
buff, width, height = session.ui_renderer.render_image_offscreen(img, rot, scale)
|
||||||
|
|
||||||
|
pixels = np.reshape(buff, (height, width, 4)).astype(np.float32) / 255.0
|
||||||
|
|
||||||
|
# gamma correction
|
||||||
|
utils.convert_colorspace(pixels, 'Linear',
|
||||||
|
'Linear' if img.is_float else img.colorspace_settings.name)
|
||||||
|
|
||||||
|
return pixels, width, height
|
||||||
|
|
||||||
|
def create_layer(base_img, pixels, img_settings, layer_settings):
|
||||||
|
base_width, base_height = base_img.size
|
||||||
|
|
||||||
|
target_width, target_height = pixels.shape[1], pixels.shape[0]
|
||||||
|
|
||||||
|
layer_img_prefix = '#layer'
|
||||||
|
layer_img_name = base_img.name + layer_img_prefix
|
||||||
|
layer_img = bpy.data.images.new(layer_img_name, width=target_width, height=target_height,
|
||||||
|
alpha=True, float_buffer=base_img.is_float)
|
||||||
|
layer_img.colorspace_settings.name = base_img.colorspace_settings.name
|
||||||
|
|
||||||
|
# gamma correction
|
||||||
|
pixels = pixels.copy()
|
||||||
|
utils.convert_colorspace(pixels,
|
||||||
|
'Linear' if img_settings['is_float']
|
||||||
|
else img_settings['colorspace_name'],
|
||||||
|
'Linear' if base_img.is_float else base_img.colorspace_settings.name)
|
||||||
|
|
||||||
|
utils.write_pixels_to_image(layer_img, pixels)
|
||||||
|
|
||||||
|
layer_img.use_fake_user = True
|
||||||
|
layer_img.pack()
|
||||||
|
|
||||||
|
img_props = base_img.imageeditorplus_properties
|
||||||
|
layers = img_props.layers
|
||||||
|
layer = layers.add()
|
||||||
|
|
||||||
|
layer.name = layer_img.name
|
||||||
|
layer.location = [int((base_width - target_width) / 2.0),
|
||||||
|
int((base_height - target_height) / 2.0)]
|
||||||
|
|
||||||
|
layer_img_postfix = \
|
||||||
|
layer_img.name[layer_img.name.rfind(layer_img_prefix) + len(layer_img_prefix):]
|
||||||
|
if layer_img_postfix:
|
||||||
|
layer.label = 'Pasted Layer ' + layer_img_postfix
|
||||||
|
else:
|
||||||
|
layer.label = 'Pasted Layer'
|
||||||
|
|
||||||
|
if layer_settings:
|
||||||
|
layer.rotation = layer_settings['rotation']
|
||||||
|
layer.scale = layer_settings['scale']
|
||||||
|
layer.custom_data = layer_settings['custom_data']
|
||||||
|
|
||||||
|
layers.move(len(layers) - 1, 0)
|
||||||
|
img_props.selected_layer_index = 0
|
||||||
|
|
||||||
|
rebuild_image_layers_nodes(base_img)
|
||||||
|
|
||||||
|
def rebuild_image_layers_nodes(img):
|
||||||
|
wm = bpy.context.window_manager
|
||||||
|
|
||||||
|
if not hasattr(wm, 'imagelayersnode_api') or wm.imagelayersnode_api.VERSION < (1, 1, 0):
|
||||||
|
return
|
||||||
|
|
||||||
|
wm.imagelayersnode_api.rebuild_image_layers_nodes(img)
|
||||||
|
|
||||||
|
def on_layer_placement_changed(self, context):
|
||||||
|
area_session = get_area_session(context)
|
||||||
|
if area_session.prevent_layer_update_event:
|
||||||
|
return
|
||||||
|
|
||||||
|
img = context.area.spaces.active.image
|
||||||
|
if not img:
|
||||||
|
return
|
||||||
|
|
||||||
|
rebuild_image_layers_nodes(img)
|
||||||
|
|
||||||
|
def on_layer_visible_changed(self, context):
|
||||||
|
refresh_image(context)
|
||||||
|
|
||||||
|
def on_selected_layer_index_changed(self, context):
|
||||||
|
if self.selected_layer_index != -1:
|
||||||
|
cancel_selection(context)
|
||||||
|
|
||||||
|
def load_icons():
|
||||||
|
global session
|
||||||
|
|
||||||
|
script_dir = os.path.dirname(os.path.realpath(__file__))
|
||||||
|
icons = bpy.utils.previews.new()
|
||||||
|
|
||||||
|
icons_dir = os.path.join(script_dir, "icons")
|
||||||
|
for file_name in os.listdir(icons_dir):
|
||||||
|
icon_name = os.path.splitext(file_name)[0]
|
||||||
|
icons.load(icon_name, os.path.join(icons_dir, file_name), 'IMAGE')
|
||||||
|
|
||||||
|
session.icons = icons
|
||||||
|
|
||||||
|
def dispose_icons():
|
||||||
|
global session
|
||||||
|
|
||||||
|
bpy.utils.previews.remove(session.icons)
|
||||||
|
|
||||||
|
def cleanup_scene():
|
||||||
|
node_group = bpy.data.node_groups.get('imageeditorplus')
|
||||||
|
if node_group:
|
||||||
|
bpy.data.node_groups.remove(node_group)
|
||||||
|
|
||||||
|
@bpy.app.handlers.persistent
|
||||||
|
def save_pre_handler(args):
|
||||||
|
cleanup_scene()
|
||||||
|
|
||||||
|
for img in bpy.data.images:
|
||||||
|
if img.source != 'VIEWER':
|
||||||
|
if img.is_dirty:
|
||||||
|
if img.packed_files or not img.filepath:
|
||||||
|
img.pack()
|
||||||
|
else:
|
||||||
|
img.save()
|
||||||
|
|
||||||
|
class IMAGE_EDITOR_PLUS_WindowPropertyGroup(bpy.types.PropertyGroup):
|
||||||
|
foreground_color: bpy.props.FloatVectorProperty(name='Foreground Color', subtype='COLOR_GAMMA',
|
||||||
|
min=0, max=1.0, size=3, default=(1.0, 1.0, 1.0))
|
||||||
|
background_color: bpy.props.FloatVectorProperty(name='Background Color', subtype='COLOR_GAMMA',
|
||||||
|
min=0, max=1.0, size=3, default=(0, 0, 0))
|
||||||
|
|
||||||
|
class IMAGE_EDITOR_PLUS_LayerPropertyGroup(bpy.types.PropertyGroup):
|
||||||
|
location: bpy.props.IntVectorProperty(size=2, update=on_layer_placement_changed)
|
||||||
|
rotation: bpy.props.FloatProperty(subtype='ANGLE', update=on_layer_placement_changed)
|
||||||
|
scale: bpy.props.FloatVectorProperty(size=2, default=(1.0, 1.0),
|
||||||
|
update=on_layer_placement_changed)
|
||||||
|
label: bpy.props.StringProperty()
|
||||||
|
hide: bpy.props.BoolProperty(name='Hide', update=on_layer_visible_changed)
|
||||||
|
custom_data: bpy.props.StringProperty(default='{}')
|
||||||
|
|
||||||
|
class IMAGE_EDITOR_PLUS_ImagePropertyGroup(bpy.types.PropertyGroup):
|
||||||
|
layers: bpy.props.CollectionProperty(type=IMAGE_EDITOR_PLUS_LayerPropertyGroup)
|
||||||
|
selected_layer_index: bpy.props.IntProperty(update=on_selected_layer_index_changed)
|
||||||
|
|
||||||
|
session = Session()
|
After Width: | Height: | Size: 473 B |
After Width: | Height: | Size: 645 B |
After Width: | Height: | Size: 654 B |
After Width: | Height: | Size: 622 B |
After Width: | Height: | Size: 428 B |
After Width: | Height: | Size: 779 B |
After Width: | Height: | Size: 679 B |
After Width: | Height: | Size: 806 B |
After Width: | Height: | Size: 642 B |
After Width: | Height: | Size: 496 B |
After Width: | Height: | Size: 537 B |
After Width: | Height: | Size: 605 B |
After Width: | Height: | Size: 579 B |
After Width: | Height: | Size: 634 B |
After Width: | Height: | Size: 649 B |
After Width: | Height: | Size: 806 B |
After Width: | Height: | Size: 553 B |
After Width: | Height: | Size: 571 B |
|
@ -0,0 +1,277 @@
|
||||||
|
'''
|
||||||
|
Copyright (C) 2020 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 sys
|
||||||
|
import math
|
||||||
|
import time
|
||||||
|
import bpy
|
||||||
|
import bgl
|
||||||
|
import blf
|
||||||
|
import gpu
|
||||||
|
from gpu_extras.batch import batch_for_shader
|
||||||
|
from mathutils import Matrix, Vector
|
||||||
|
import numpy as np
|
||||||
|
|
||||||
|
default_vertex_shader = '''
|
||||||
|
uniform mat4 ModelViewProjectionMatrix;
|
||||||
|
|
||||||
|
in vec2 pos;
|
||||||
|
|
||||||
|
void main()
|
||||||
|
{
|
||||||
|
gl_Position = ModelViewProjectionMatrix * vec4(pos, 0, 1.0);
|
||||||
|
}
|
||||||
|
'''
|
||||||
|
|
||||||
|
default_fragment_shader = '''
|
||||||
|
uniform vec4 color;
|
||||||
|
|
||||||
|
out vec4 fragColor;
|
||||||
|
|
||||||
|
void main()
|
||||||
|
{
|
||||||
|
fragColor = color;
|
||||||
|
}
|
||||||
|
'''
|
||||||
|
|
||||||
|
dotted_line_vertex_shader = '''
|
||||||
|
uniform mat4 ModelViewProjectionMatrix;
|
||||||
|
|
||||||
|
in vec2 pos;
|
||||||
|
in float arcLength;
|
||||||
|
|
||||||
|
out float arcLengthOut;
|
||||||
|
|
||||||
|
void main()
|
||||||
|
{
|
||||||
|
arcLengthOut = arcLength;
|
||||||
|
|
||||||
|
gl_Position = ModelViewProjectionMatrix * vec4(pos, 0, 1.0);
|
||||||
|
}
|
||||||
|
'''
|
||||||
|
|
||||||
|
dotted_line_fragment_shader = '''
|
||||||
|
uniform float scale;
|
||||||
|
uniform float offset;
|
||||||
|
uniform vec4 color1;
|
||||||
|
uniform vec4 color2;
|
||||||
|
|
||||||
|
in float arcLengthOut;
|
||||||
|
|
||||||
|
out vec4 fragColor;
|
||||||
|
|
||||||
|
void main()
|
||||||
|
{
|
||||||
|
if (step(sin((arcLengthOut + offset) * scale), 0.5) == 1) {
|
||||||
|
fragColor = color1;
|
||||||
|
} else {
|
||||||
|
fragColor = color2;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
'''
|
||||||
|
|
||||||
|
image_vertex_shader = '''
|
||||||
|
uniform mat4 ModelViewProjectionMatrix;
|
||||||
|
|
||||||
|
in vec2 pos;
|
||||||
|
in vec2 texCoord;
|
||||||
|
|
||||||
|
out vec2 texCoordOut;
|
||||||
|
|
||||||
|
void main()
|
||||||
|
{
|
||||||
|
gl_Position = ModelViewProjectionMatrix * vec4(pos, 0, 1.0);
|
||||||
|
texCoordOut = texCoord;
|
||||||
|
}
|
||||||
|
'''
|
||||||
|
|
||||||
|
image_fragment_shader = '''
|
||||||
|
uniform sampler2D image;
|
||||||
|
|
||||||
|
in vec2 texCoordOut;
|
||||||
|
|
||||||
|
out vec4 fragColor;
|
||||||
|
|
||||||
|
void main()
|
||||||
|
{
|
||||||
|
fragColor = texture(image, texCoordOut);
|
||||||
|
}
|
||||||
|
'''
|
||||||
|
|
||||||
|
def make_scale_matrix(scale):
|
||||||
|
return Matrix([
|
||||||
|
[scale[0], 0, 0, 0],
|
||||||
|
[0, scale[1], 0, 0],
|
||||||
|
[0, 0, 1.0, 0],
|
||||||
|
[0, 0, 0, 1.0]
|
||||||
|
])
|
||||||
|
|
||||||
|
class UIRenderer:
|
||||||
|
def __init__(self):
|
||||||
|
self.default_shader = gpu.types.GPUShader(default_vertex_shader,
|
||||||
|
default_fragment_shader)
|
||||||
|
self.default_shader_u_color = self.default_shader.uniform_from_name("color")
|
||||||
|
|
||||||
|
self.dotted_line_shader = gpu.types.GPUShader(dotted_line_vertex_shader,
|
||||||
|
dotted_line_fragment_shader)
|
||||||
|
self.dotted_line_shader_u_color1 = self.dotted_line_shader.uniform_from_name("color1")
|
||||||
|
self.dotted_line_shader_u_color2 = self.dotted_line_shader.uniform_from_name("color2")
|
||||||
|
|
||||||
|
#self.image_shader = gpu.shader.from_builtin('2D_IMAGE')
|
||||||
|
self.image_shader = gpu.types.GPUShader(image_vertex_shader,
|
||||||
|
image_fragment_shader)
|
||||||
|
|
||||||
|
def render_selection_frame(self, pos, size, rot=0, scale=(1.0, 1.0)):
|
||||||
|
width, height = size[0], size[1]
|
||||||
|
|
||||||
|
bgl.glEnable(bgl.GL_BLEND)
|
||||||
|
bgl.glLineWidth(2.0)
|
||||||
|
|
||||||
|
with gpu.matrix.push_pop():
|
||||||
|
verts = [[0, 0], [0, height], [width, height], [width, 0], [0, 0]]
|
||||||
|
# T <= R <= S <= centering
|
||||||
|
mat = Matrix.Translation([pos[0] + width / 2.0, pos[1] + height / 2.0, 0]) \
|
||||||
|
@ Matrix.Rotation(rot, 4, 'Z') \
|
||||||
|
@ make_scale_matrix(scale) \
|
||||||
|
@ Matrix.Translation([-width / 2.0, -height / 2.0, 0])
|
||||||
|
|
||||||
|
for i, vert in enumerate(verts):
|
||||||
|
verts[i] = (mat @ Vector(vert + [0, 1]))[:2]
|
||||||
|
|
||||||
|
verts = np.array(verts, 'f')
|
||||||
|
|
||||||
|
arc_lengths = [0]
|
||||||
|
for a, b in zip(verts[:-1], verts[1:]):
|
||||||
|
arc_lengths.append(arc_lengths[-1] + np.linalg.norm(a - b))
|
||||||
|
|
||||||
|
batch = batch_for_shader(self.dotted_line_shader, 'LINE_STRIP',
|
||||||
|
{"pos": verts, "arcLength": arc_lengths})
|
||||||
|
|
||||||
|
self.dotted_line_shader.bind()
|
||||||
|
|
||||||
|
self.dotted_line_shader.uniform_float("scale", 0.6)
|
||||||
|
self.dotted_line_shader.uniform_float("offset", 0)
|
||||||
|
self.dotted_line_shader.uniform_vector_float(self.dotted_line_shader_u_color1,
|
||||||
|
np.array([1.0, 1.0, 1.0, 0.5], 'f'), 4)
|
||||||
|
self.dotted_line_shader.uniform_vector_float(self.dotted_line_shader_u_color2,
|
||||||
|
np.array([0.0, 0.0, 0.0, 0.5], 'f'), 4)
|
||||||
|
|
||||||
|
batch.draw(self.dotted_line_shader)
|
||||||
|
|
||||||
|
err = bgl.glGetError()
|
||||||
|
if err != bgl.GL_NO_ERROR:
|
||||||
|
print('render_selection_frame')
|
||||||
|
print('OpenGL error:', err)
|
||||||
|
|
||||||
|
def render_image_sub(self, img, pos, size, rot, scale):
|
||||||
|
width, height = size[0], size[1]
|
||||||
|
|
||||||
|
img.gl_load()
|
||||||
|
|
||||||
|
bgl.glActiveTexture(bgl.GL_TEXTURE0)
|
||||||
|
bgl.glBindTexture(bgl.GL_TEXTURE_2D, img.bindcode)
|
||||||
|
bgl.glTexParameteri(bgl.GL_TEXTURE_2D, bgl.GL_TEXTURE_MIN_FILTER, bgl.GL_NEAREST)
|
||||||
|
bgl.glTexParameteri(bgl.GL_TEXTURE_2D, bgl.GL_TEXTURE_MAG_FILTER, bgl.GL_NEAREST)
|
||||||
|
|
||||||
|
with gpu.matrix.push_pop():
|
||||||
|
gpu.matrix.translate([pos[0] + width / 2.0, pos[1] + height / 2.0])
|
||||||
|
gpu.matrix.multiply_matrix(
|
||||||
|
Matrix.Rotation(rot, 4, 'Z'))
|
||||||
|
gpu.matrix.scale(scale)
|
||||||
|
gpu.matrix.translate([-width / 2.0, -height / 2.0])
|
||||||
|
|
||||||
|
batch = batch_for_shader(self.image_shader, 'TRI_FAN',
|
||||||
|
{
|
||||||
|
"pos": [
|
||||||
|
(0, 0),
|
||||||
|
(width, 0),
|
||||||
|
size,
|
||||||
|
(0, height)
|
||||||
|
],
|
||||||
|
"texCoord": [(0, 0), (1, 0), (1, 1), (0, 1)]
|
||||||
|
})
|
||||||
|
|
||||||
|
self.image_shader.bind()
|
||||||
|
|
||||||
|
self.image_shader.uniform_int('image', 0)
|
||||||
|
|
||||||
|
batch.draw(self.image_shader)
|
||||||
|
|
||||||
|
err = bgl.glGetError()
|
||||||
|
if err != bgl.GL_NO_ERROR:
|
||||||
|
print('render_image')
|
||||||
|
print('OpenGL error:', err)
|
||||||
|
|
||||||
|
def render_image(self, img, pos, size, rot=0, scale=(1.0, 1.0)):
|
||||||
|
bgl.glEnable(bgl.GL_BLEND)
|
||||||
|
# FIXME: glBlendFuncSeparate does not seem to be implemented,
|
||||||
|
# but we believe this blend mode was already applied
|
||||||
|
#bgl.glBlendFuncSeparate(bgl.GL_SRC_ALPHA, bgl.GL_ONE_MINUS_SRC_ALPHA,
|
||||||
|
# bgl.GL_ONE, bgl.GL_ONE_MINUS_SRC_ALPHA);
|
||||||
|
|
||||||
|
self.render_image_sub(img, pos, size, rot, scale)
|
||||||
|
|
||||||
|
def render_image_offscreen(self, img, rot=0, scale=(1.0, 1.0)):
|
||||||
|
width, height = img.size[0], img.size[1]
|
||||||
|
|
||||||
|
box = [[0, 0], [width, 0], [0, height], [width, height]]
|
||||||
|
mat = Matrix.Rotation(rot, 4, 'Z') \
|
||||||
|
@ make_scale_matrix(scale) \
|
||||||
|
@ Matrix.Translation([-width / 2.0, -height / 2.0, 0])
|
||||||
|
min_x, min_y = sys.float_info.max, sys.float_info.max
|
||||||
|
max_x, max_y = -sys.float_info.max, -sys.float_info.max
|
||||||
|
|
||||||
|
# calculate bounding box
|
||||||
|
for pos in box:
|
||||||
|
pos = mat @ Vector(pos + [0, 1])
|
||||||
|
min_x = min(min_x, pos[0])
|
||||||
|
min_y = min(min_y, pos[1])
|
||||||
|
max_x = max(max_x, pos[0])
|
||||||
|
max_y = max(max_y, pos[1])
|
||||||
|
|
||||||
|
ofs_width = math.ceil(max_x - min_x)
|
||||||
|
ofs_height = math.ceil(max_y - min_y)
|
||||||
|
|
||||||
|
ofs = gpu.types.GPUOffScreen(ofs_width, ofs_height)
|
||||||
|
with ofs.bind():
|
||||||
|
bgl.glDisable(bgl.GL_BLEND)
|
||||||
|
|
||||||
|
bgl.glClearColor(0, 0, 0, 0)
|
||||||
|
bgl.glClear(bgl.GL_COLOR_BUFFER_BIT)
|
||||||
|
|
||||||
|
with gpu.matrix.push_pop():
|
||||||
|
gpu.matrix.load_projection_matrix(Matrix.Identity(4))
|
||||||
|
|
||||||
|
gpu.matrix.load_identity()
|
||||||
|
gpu.matrix.scale([1.0 / (ofs_width / 2.0), 1.0 / (ofs_height / 2.0)])
|
||||||
|
gpu.matrix.translate([-width / 2.0, -height / 2.0])
|
||||||
|
|
||||||
|
self.render_image_sub(img, [0, 0], [width, height], rot, scale)
|
||||||
|
|
||||||
|
buff = bgl.Buffer(bgl.GL_BYTE, ofs_width * ofs_height * 4)
|
||||||
|
bgl.glReadBuffer(bgl.GL_BACK)
|
||||||
|
bgl.glReadPixels(0, 0, ofs_width, ofs_height, bgl.GL_RGBA,
|
||||||
|
bgl.GL_UNSIGNED_BYTE, buff)
|
||||||
|
|
||||||
|
ofs.free()
|
||||||
|
|
||||||
|
err = bgl.glGetError()
|
||||||
|
if err != bgl.GL_NO_ERROR:
|
||||||
|
print('render_image_offscreen')
|
||||||
|
print('OpenGL error:', err)
|
||||||
|
|
||||||
|
return buff, ofs_width, ofs_height
|
|
@ -0,0 +1,145 @@
|
||||||
|
'''
|
||||||
|
Copyright (C) 2021 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 bpy
|
||||||
|
import numpy as np
|
||||||
|
|
||||||
|
def read_pixels_from_image(img):
|
||||||
|
width, height = img.size[0], img.size[1]
|
||||||
|
|
||||||
|
if bpy.app.version >= (2, 83, 0):
|
||||||
|
pixels = np.empty(len(img.pixels), dtype=np.float32);
|
||||||
|
img.pixels.foreach_get(pixels)
|
||||||
|
return np.reshape(pixels, (height, width, 4))
|
||||||
|
else:
|
||||||
|
return np.reshape(img.pixels[:], (height, width, 4))
|
||||||
|
|
||||||
|
def write_pixels_to_image(img, pixels):
|
||||||
|
if bpy.app.version >= (2, 83, 0):
|
||||||
|
img.pixels.foreach_set(np.reshape(pixels, -1))
|
||||||
|
else:
|
||||||
|
img.pixels = np.reshape(pixels, -1)
|
||||||
|
|
||||||
|
if img.preview:
|
||||||
|
img.preview.reload()
|
||||||
|
|
||||||
|
def rgb_to_hsl(rgb):
|
||||||
|
red = rgb[:, :, 0]
|
||||||
|
green = rgb[:, :, 1]
|
||||||
|
blue = rgb[:, :, 2]
|
||||||
|
|
||||||
|
max_chan = np.maximum(np.maximum(red, green), blue)
|
||||||
|
min_chan = np.minimum(np.minimum(red, green), blue)
|
||||||
|
sum = max_chan + min_chan
|
||||||
|
light = sum / 2.0
|
||||||
|
|
||||||
|
diff = max_chan - min_chan
|
||||||
|
|
||||||
|
sat_denom = 1.0 - np.abs(sum - 1.0)
|
||||||
|
sat_denom_safe = np.where(sat_denom == 0, 1.0, sat_denom) # for div by 0
|
||||||
|
|
||||||
|
sat = diff / sat_denom_safe
|
||||||
|
|
||||||
|
diff_safe = np.where(diff == 0, 1.0, diff) # for div by 0
|
||||||
|
|
||||||
|
hue_0 = np.zeros((rgb.shape[0], rgb.shape[1]))
|
||||||
|
hue_0 = np.where(np.equal(max_chan, red), (green - blue) / diff_safe, hue_0)
|
||||||
|
hue_0 = np.where(np.equal(max_chan, green), 2.0 + (blue - red) / diff_safe, hue_0)
|
||||||
|
hue_0 = np.where(np.equal(max_chan, blue), 4.0 + (red - green) / diff_safe, hue_0)
|
||||||
|
|
||||||
|
hue = (hue_0 / 6.0) % 1.0
|
||||||
|
|
||||||
|
return np.dstack((hue, sat, light))
|
||||||
|
|
||||||
|
def hsl_to_rgb(hsl):
|
||||||
|
hue = hsl[:, :, 0]
|
||||||
|
sat = hsl[:, :, 1]
|
||||||
|
light = hsl[:, :, 2]
|
||||||
|
|
||||||
|
c = (1.0 - np.abs(light * 2.0 - 1.0)) * sat
|
||||||
|
|
||||||
|
index_f = hue * 6.0
|
||||||
|
index = (hue * 6.0).astype(int)
|
||||||
|
|
||||||
|
x = c * (1.0 - np.abs(index_f % 2.0 - 1.0))
|
||||||
|
|
||||||
|
zeros = np.zeros((index.shape[0], index.shape[1]))
|
||||||
|
red_0 = index.choose((c, x, zeros, zeros, x, c))
|
||||||
|
green_0 = index.choose((x, c, c, x, zeros, zeros))
|
||||||
|
blue_0 = index.choose((zeros, zeros, x, c, c, x))
|
||||||
|
|
||||||
|
m = light - c / 2.0
|
||||||
|
|
||||||
|
return np.dstack((red_0 + m, green_0 + m, blue_0 + m))
|
||||||
|
|
||||||
|
def straight_to_premul_alpha(pixels):
|
||||||
|
alpha_chan = pixels[:, :, 3:]
|
||||||
|
|
||||||
|
return np.dstack((pixels[:, :, :3] * alpha_chan, alpha_chan))
|
||||||
|
|
||||||
|
def premul_to_straight_alpha(pixels):
|
||||||
|
alpha_chan = pixels[:, :, 3:]
|
||||||
|
new_color_chan = pixels[:, :, :3] \
|
||||||
|
/ np.where(alpha_chan == 0, 1.0, alpha_chan) # for div by 0
|
||||||
|
|
||||||
|
return np.dstack((new_color_chan, alpha_chan))
|
||||||
|
|
||||||
|
def gaussian_blur_core(pixels, blur_size):
|
||||||
|
|
||||||
|
# straight => premul alpha
|
||||||
|
pixels = straight_to_premul_alpha(pixels)
|
||||||
|
|
||||||
|
height, width = pixels.shape[0], pixels.shape[1]
|
||||||
|
|
||||||
|
blur_size_safe = 1.0 if blur_size == 0 else blur_size
|
||||||
|
|
||||||
|
kernel_size = 2 * int(4 * blur_size + 0.5) + 1
|
||||||
|
|
||||||
|
kernel = np.zeros((kernel_size))
|
||||||
|
|
||||||
|
h_kernel_size = kernel_size // 2
|
||||||
|
for x in range(-h_kernel_size, h_kernel_size + 1):
|
||||||
|
kernel[x + h_kernel_size] = np.exp(-(x ** 2)/(2 * blur_size_safe ** 2))
|
||||||
|
kernel = kernel / np.sum(kernel)
|
||||||
|
|
||||||
|
pixels_pad = np.pad(pixels, ((kernel_size // 2, kernel_size // 2),
|
||||||
|
(kernel_size // 2, kernel_size // 2), (0, 0)), 'edge')
|
||||||
|
height_pad, width_pad = pixels_pad.shape[0], pixels_pad.shape[1]
|
||||||
|
|
||||||
|
gaus_y = np.zeros((height_pad, width, 4))
|
||||||
|
for x, v in enumerate(kernel):
|
||||||
|
gaus_y += v * pixels_pad[:, x:width + x]
|
||||||
|
|
||||||
|
gaus_x = np.zeros((height, width, 4))
|
||||||
|
for y, v in enumerate(kernel):
|
||||||
|
gaus_x += v * gaus_y[y:height + y]
|
||||||
|
|
||||||
|
new_pixels = np.clip(gaus_x, 0, 1.0)
|
||||||
|
|
||||||
|
# premul => straight alpha
|
||||||
|
return premul_to_straight_alpha(new_pixels)
|
||||||
|
|
||||||
|
def convert_colorspace(pixels, src_colorspace, dest_colorspace):
|
||||||
|
if src_colorspace == dest_colorspace:
|
||||||
|
return
|
||||||
|
|
||||||
|
if src_colorspace == 'Linear' and dest_colorspace == 'sRGB':
|
||||||
|
pixels[:, :, 0:3] = pixels[:, :, :3] ** (1.0 / 2.2)
|
||||||
|
elif src_colorspace == 'sRGB' and dest_colorspace == 'Linear':
|
||||||
|
pixels[:, :, 0:3] = pixels[:, :, :3] ** 2.2
|
||||||
|
|
||||||
|
# unsupported conversion
|