002.09 簡單 PNG 圖片編輯器

Jason990420發表於2020-01-04

檔案建立日期: 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圖片編輯器, 剛好夠自己使用就行. 因為圖片的畫素被稍放大成格子, 所以所有的編輯動作都要自己作, 再加上透明色要另外處理, 內容就變得複雜起來, 沒有作太多測試, 如果有任何問題敬請提示, 謝謝 !

軟體要求:

  1. 可畫點, 線, 方框, 圓
  2. 可填色
  3. 固定只用到27種顏色, 另加上透明色
  4. 可讀取現在圖檔
  5. 編輯後可以存檔

輸出畫面:

002.09 簡單PNG圖片編輯器

程式碼

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 協議》,轉載必須註明作者和本文連結

Jason Yang

相關文章