檔案建立日期: 2020/01/04
最後修訂日期: None
相關軟體資訊:
| Windows 10 | Python 3.8.1 | PySimpleGUI 4.14.1 | Numpy 1.18.0 | Pillow 6.2.1 |
參考檔案
說明: 所有內容歡迎引用, 只需註明來源及作者, 本文內容如有錯誤或用詞不當, 敬請指正.
標題: 002.09 簡單PNG圖片編輯器
前言
最近常用到PNG圖片, 上網不太容易找到適用的整組圖片, 於是就花了一點時間, 寫了一個簡單的PNG圖片編輯器, 剛好夠自己使用就行. 因為圖片的畫素被稍放大成格子, 所以所有的編輯動作都要自己作, 再加上透明色要另外處理, 內容就變得複雜起來, 沒有作太多測試, 如果有任何問題敬請提示, 謝謝 !
軟體要求:
- 可畫點, 線, 方框, 圓
- 可填色
- 固定只用到27種顏色, 另加上透明色
- 可讀取現在圖檔
- 編輯後可以存檔
輸出畫面:
程式碼
import sys
import PySimpleGUI as sg
import numpy as np
from math import sqrt
from PIL import Image, ImageOps
if 'Jason' in globals(): # Create Personel Utility
del Jason # Always reload
if 'Jason_UTIL' in sys.modules:
del sys.modules['Jason_UTIL']
from Jason_UTIL import Jason
class PNG():
"""
Class PNG - Built all functions, constants and Variable required for
processing of PNG file, image and display.
"""
def __init__(self):
self.Font = 'Courier 16' # All font, except
self.Font2 = 'Courier 12' # font for transparent
self.File_Type = (("ALL PNG Files", "*.png"),) # File type - *.png
self.Scale = 8 # cell width/height
self.Pixels_Max = 96 # Max PNG size
self.Pixels = 48 # Default PNG size
self.Height = self.Pixels_Max * self.Scale # Canvas height
self.Width = self.Height # Canvas width
self.Start = (self.Pixels_Max - self.Pixels)//2 # Start of PNG
self.Stop = self.Start + self.Pixels # Stop of PNG
self.Color = [0, 0, 0, 255] # Default Color - black
self.Grey = [192, 192, 192, 0] # Default Transparent
self.Mode = 'Point' # Default Mode - Point
self.Color_Table = ['00', '80', 'FF'] # Color Table to join
self.PNG_Size = [str(i)+'x'+str(i) # Values for Combo
for i in range(8, self.Pixels_Max + 8, 8)] # 8 ~ 96, 8/step
self.Object = np.zeros( # Cell ID
(self.Pixels_Max, self.Pixels_Max), dtype=np.uint32)
self.Photo = np.full( # PNG color - RGBA
(self.Pixels_Max, self.Pixels_Max, 4), 255, dtype=np.uint8)
def Button(self, Key): # Button function
return sg.Button(
button_text=Key, enable_events=True, border_width=1, size=(10, 1),
font=self.Font, key=Key, pad=(0, 5))
def Button_Color(self, color): # Color Table Button
return sg.Button(
button_text='', enable_events=True, border_width=1, size=(3, 1),
button_color=('white', color), font=self.Font, pad=(0, 0),
key=color)
def Button_Transparent(self): # Trasparent Button
return sg.Button(
button_text='Transparent', enable_events=True, border_width=1,
size=(13, 1), button_color=('white', 'blue'), font=self.Font2,
key='Transparent', pad=(0, 5))
def Canvas(self, Width, Height, Pad, Key, drag=True): # Canvas for cells
return sg.Graph(
canvas_size=(Width, Height), key=Key, pad=Pad, drag_submits=drag,
graph_bottom_left=(0, 0), graph_top_right=(Width, Height),
background_color='#000000', enable_events=True, )
def Check(self, temp, Checked, List): # Check duplicated
for Point in List:
y, x = Point
if ((Point not in Checked) and (Point not in temp) and
(self.Start <= x < self.Stop) and
(self.Start <= y < self.Stop)):
temp += [Point]
return temp
def Color_Array(self): # Color Table Buttons
Array = []
for i in self.Color_Table:
for j in self.Color_Table:
Line = []
for k in self.Color_Table:
color = ''.join(('#', i, j, k))
Line.append(self.Button_Color(color))
Array.append(Line)
return Array
def Column(self, Layout, Key): # Container for Col
return sg.Column(layout=Layout, background_color='green', pad=(5, 5),
key=Key)
def Combo(self, Default, Values): # Select for PNG size
return sg.Combo(
default_value=Default, values=Values, size=(9, 1), font=P.Font,
pad=(0, 10), enable_events=True, key='Combo')
def Draw_Box(self, Point1, Point2, replace=True): # Draw Rectangle
(x0, y0), (x1, y1) = Point1, Point2
Step_x = 1 if x1 > x0 else -1
Step_y = 1 if y1 > y0 else -1
for x in range(x0, x1 + Step_x, Step_x):
self.Pixel((x, y0), replace=replace)
for y in range(y0 + Step_y, y1 + Step_y, Step_y):
self.Pixel((x1, y), replace=replace)
for x in range(x1 - Step_x, x0 - Step_x, -Step_x):
self.Pixel((x, y1), replace=replace)
for y in range(y1 - Step_y, y0, -Step_y):
self.Pixel((x0, y), replace=replace)
def Draw_Circle(self, Point1, Point2, replace=True): # Draw Circle
(x0, y0), (x1, y1) = Point1, Point2
Radius = sqrt( (x1 - x0)**2 + (y1 - y0)**2 )
f = 1 - Radius
ddf_x = 1
ddf_y = -2 * Radius
x = 0
y = Radius
List = [[x0, int(y0 + Radius + 0.5)], [x0, int(y0 - Radius + 0.5)],
[int(x0 + Radius + 0.5), y0], [int(x0 - Radius + 0.5), y0]]
while x < y:
if f >= 0:
y -= 1
ddf_y += 2
f += ddf_y
x += 1
ddf_x += 2
f += ddf_x
List += [[x0 + x, int(y0 + y + 0.5)], [x0 - x, int(y0 + y + 0.5)],
[x0 + x, int(y0 - y + 0.5)], [x0 - x, int(y0 - y + 0.5)],
[int(x0 + y + 0.5), y0 + x], [int(x0 - y + 0.5), y0 + x],
[int(x0 + y + 0.5), y0 - x], [int(x0 - y + 0.5), y0 - x]]
List = set(tuple(element) for element in List)
for Point in List:
self.Pixel(Point, replace=replace)
def Draw_Fill(self, Point1, replace=True): # Fill area
y, x = Point1
if not ((self.Start <= x < self.Stop) and
(self.Start <= y < self.Stop)):
return
Color = self.Photo[y, x]
Points = []
Checked = []
To_Checked = [Point1]
while True:
if To_Checked == []:
break
temp = []
for i in range(len(To_Checked)):
Point = To_Checked[i]
y1, x1 = Point
Checked += [Point]
if list(self.Photo[y1, x1]) == list(Color):
Points += [Point]
else:
continue
temp = self.Check(temp, Checked,
[[y1-1, x1], [y1+1, x1], [y1, x1-1], [y1, x1+1]])
To_Checked = temp
for Point in Points:
y, x = Point
self.Photo[y, x] = self.Color
Draw.Erase()
P.Draw_Pixels()
def Draw_Line(self, Point1, Point2, replace=True): # Draw Line
(x0, y0), (x1, y1) = Point1, Point2
dx = abs(x1 - x0)
dy = abs(y1 - y0)
x, y = x0, y0
sx = -1 if x0 > x1 else 1
sy = -1 if y0 > y1 else 1
if dx > dy:
err = dx
while x != x1:
self.Pixel((x, y), replace=replace)
err -= dy * 2
if err < 0:
y += sy
err += dx * 2
x += sx
else:
err = dy
while y != y1:
self.Pixel((x, y), replace=replace)
err -= dx * 2
if err < 0:
x += sx
err += dy * 2
y += sy
self.Pixel((x, y), replace=replace)
def Draw_Pixel(self, Point): # Draw cell from Array
y, x = Point
if (self.Start <= x < self.Stop) and (self.Start <= y < self.Stop):
if self.Photo[y, x, 3] == 0:
Fill = Jason.RGB_To_HEX(self.Grey)
self.Photo[y, x] = self.Grey
else:
Fill = Jason.RGB_To_HEX(self.Photo[y, x])
Line = 'black'
Line_Width = 1
else:
Fill = Jason.RGB_To_HEX(self.Grey)
Line = None
Line_Width = 0
return Draw.DrawRectangle(
(x * self.Scale, self.Height - y * self.Scale),
((x + 1) * self.Scale, self.Height - (y + 1) * self.Scale),
fill_color=Fill, line_color=Line, line_width=Line_Width)
def Draw_Pixels(self): # Draw all cells
for y in range(self.Pixels_Max):
for x in range(self.Pixels_Max):
self.Object[y, x] = self.Draw_Pixel([y, x])
return
def File_Browse(self, Key): # Open file
return sg.FileBrowse(
button_text=Key, target=Key, size=(10, 1), enable_events=True,
font=self.Font, key=Key, pad=(0, 5), file_types=self.File_Type)
def File_Save_As(self, Key): # Save file
return sg.FileSaveAs(
button_text=Key, target=Key, size=(10, 1), enable_events=True,
font=self.Font, key=Key, pad=(0, 5), file_types=self.File_Type)
def Get_Object_Position(self, Point): # Get cell with mouse
Object_List = Draw.GetFiguresAtLocation(Point)
if Object_List != ():
return Jason.Index_2D(self.Object, Object_List[-1])
def Open_PNG(self, filename): # Open/Load PNG file
if filename == '' or not filename.endswith('.png'):
return
try:
im = Image.open(filename)
except:
sg.Popup(
'File '+str(filename)+' failed to open !',
title='Error', font=self.Font)
return
print(im.mode)
if im.width != im.height:
sg.Popup(
"Width {} not same as Height {} !".format(im.width, im.height),
title='Error', font=self.Font)
return
if im.width not in [i for i in range(8, self.Pixels_Max + 8, 8)]:
sg.Popup(
'Dim {} not in (8, 16, ..., {}) !'.format(im.width,
self.Pixels_Max),
title='Error', font=self.Font)
return
if im.mode == 'LA':
L, A = im.split()
L = ImageOps.invert(A)
im = Image.merge('LA', (L, A))
if im.mode != 'RGBA':
im = im.convert('RGBA')
self.Pixels = im.width
self.Start = (self.Pixels_Max - self.Pixels)//2
self.Stop = self.Start + self.Pixels
self.Photo[self.Start:self.Stop, self.Start:self.Stop] = np.array(
im, dtype=np.uint8)
Window['Combo'].Update(value=self.PNG_Size[im.width//8-1])
Draw.Erase()
self.Draw_Pixels()
def Radio(self, Key, default=False): # Select Method/Mode
return sg.Radio(Key, group_id='Function', default=default, pad=(0, 5),
text_color='white', background_color='green',
key=Key, enable_events=True, font=self.Font)
def Save_PNG(self, filename): # Save Image to PNG
if filename == '':
return
if not filename.endswith('.png'):
filename += '.png'
im = Image.fromarray(
self.Photo[
self.Start:self.Stop, self.Start:self.Stop], mode='RGBA')
try:
im.save(filename, format='PNG')
except:
sg.Popup(
'{} failed to save !'.format(filename),
title='Error', font=self.Font)
def Pixel(self, Point, replace=True): # Draw Pixel by Color
y, x = Point
if (self.Start <= x < self.Stop) and (self.Start <= y < self.Stop):
r, g, b, a = 255-self.Photo[y, x]
if replace:
r, g, b, a = self.Color
New_Color = Jason.RGB_To_HEX((r, g, b))
Draw.DeleteFigure(self.Object[y, x])
Figure = Draw.DrawRectangle(
(x * self.Scale, self.Height - y * self.Scale),
((x + 1) * self.Scale, self.Height - (y + 1) * self.Scale),
fill_color=New_Color, line_color='black', line_width=1)
self.Object[y, x] = Figure
self.Photo[y, x] = [r, g, b, a]
P = PNG()
Layout0 = ([[P.File_Browse('Open')], [P.File_Save_As('Save')], # Column 0
[P.Combo('48x48', P.PNG_Size)]] + P.Color_Array() +
[[P.Button_Transparent()]])
Layout1 = [[P.Canvas(P.Width, P.Height, (0,0), 'Graph1')]] # Column 1
Layout2 = [[P.Button('Clear')], [P.Radio('Point', default=True)], # Column 2
[P.Radio('Line')], [P.Radio('Circle')],
[P.Radio('Box')], [P.Radio('Fill')],
[sg.Text('', size=(10, 5), font=P.Font,
background_color=Jason.RGB_To_HEX(P.Color), key='Color',
pad=(0, 5), border_width=1, relief='solid')]]
Layout = [[P.Column(Layout0, 'Column0'), # Layout of window
P.Column(Layout1, 'Column1'),
P.Column(Layout2, 'Column2')]]
Window = sg.Window( 'Draw PNG', layout=Layout, background_color='green',
margins=(0, 0), finalize=True) # Create window
Draw = Window['Graph1']
P.Draw_Pixels()
Function = {'Point':P.Pixel, 'Line':P.Draw_Line, 'Circle':P.Draw_Circle,
'Box':P.Draw_Box, 'Fill':P.Draw_Fill} # Function by event
Drag = False # Drag Mode
Click = False # Mouse Down
Drawn = False # Object drawn
while True:
Event, Values = Window.read() # Event read
if Event == None: # Close window
break
elif Event == 'Open': # Event Open
filename = Values['Open'].lower()
P.Open_PNG(filename)
elif Event == 'Save': # Event Save
filename = Values['Save'].lower()
P.Save_PNG(filename)
elif Event[0] == '#': # Event Color Table
P.Color = (int(Event[1:3], 16), int(Event[3:5], 16),
int(Event[5:7], 16), 255)
Window['Color'].Update(background_color=Event)
elif Event == 'Combo': # Event PNG size
P.Pixels = int(Values['Combo'][:Values['Combo'].find('x')])
P.Start = (P.Pixels_Max - P.Pixels)//2
P.Stop = P.Start + P.Pixels
P.Photo[P.Start:P.Stop, P.Start:P.Stop, :] = 255
Draw.Erase()
P.Draw_Pixels()
elif Event == 'Clear': # Event Clear PNG
P.Photo[P.Start:P.Stop, P.Start:P.Stop, :] = 255
Draw.Erase()
P.Draw_Pixels()
elif Event == 'Transparent': # Event Trans.
P.Color = P.Grey
Window['Color'].Update(background_color=Jason.RGB_To_HEX(P.Color))
elif Event in ['Point', 'Line', 'Circle', 'Box', 'Fill']: # Event Mode
P.Mode = Event
elif Event == 'Graph1': # Event on Canvas
if not Drag: # Mouse Down
Start_Point = Values['Graph1']
Start_Index = P.Get_Object_Position(Start_Point)
if not Click:
Click = True
Old_Point = Start_Point
else:
if Start_Point != Old_Point:
Drag = True
else: # Drag Mouse
Stop_Point = Values['Graph1']
Stop_Index = P.Get_Object_Position(Stop_Point)
if None in [Start_Index, Stop_Index]:
pass
else:
if P.Mode in ['Line', 'Circle', 'Box']: # Move Object
if Drawn:
Function[P.Mode](Old_Start, Old_Stop, replace=False)
Function[P.Mode](Start_Index, Stop_Index, replace=False)
Old_Start, Old_Stop = Start_Index, Stop_Index
Window.Refresh()
Drawn = True
elif Event == 'Graph1+UP': # Mouse Up
if not Drag: # Point/Fill
if P.Mode in ['Point', 'Fill'] and Start_Index != None:
Function[P.Mode](Start_Index)
elif None not in [Start_Index, Stop_Index]: # Line/Circle/Box
if P.Mode in ['Line', 'Circle', 'Box']:
if Drawn:
Function[P.Mode](Old_Start, Old_Stop, replace=False)
Function[P.Mode](Start_Index, Stop_Index)
else:
pass
Drag = False # Reset Mouse
Click = False
Drawn = False
Window.close()
"""
Jason_Utility define some methods or functions which often used.
It will create an instance Jason. You can use it by:
1. from Jason_Utility import Jason
2. Jason.method() or Jason.function()
3. Most of functions just return the result.
4. Most of functions names as Function_Type
"""
import numpy as np
class Jason_Utility():
"""
Jason_Utility used internally, not called by user.
"""
def __init__(self):
self.version = self.__version__ = '0.1'
self.Date = '2020/01/01'
def Index_2D(self, List, Value):
y = 0
for line in List:
x = 0
for point in line:
if point == Value:
return [y, x]
x += 1
y += 1
return None
def Int(self, List):
return list((np.array(List)+0.5).astype(np.uint8))
def Add(self, Parent, Child):
if Child not in Parent:
Parent += [Child]
return Parent
def RGB_To_HEX(self, Color):
return "#%02x%02x%02x" % tuple(Color[:3])
"""
from Jason import Jason to use some useful methods and functions
"""
Jason = Jason_Utility()
本作品採用《CC 協議》,轉載必須註明作者和本文連結