【Python】用鼠标点出像素画工具

查看 69|回复 9
作者:DeviLeo   
0x0 前言
老母亲要给小孙女的衣服上缝上图案,于是去百度搜了几张图,画质堪忧,格子也没有标序号,几乎不能看。
想着做个小工具,一来练练手,二来方便修改,导出图片后打印也清晰。
0x1 环境
  • python3.7+
  • pillow
  • pyqt6

    # 安装pillow
    $ python3 -m pip install pillow
    # 安装pyqt6
    $ python3 -m pip install pyside6
    0x2 核心代码
    Stitch类主要实现了像素画棋盘格的大小、位置的计算,根据传入的鼠标位置获取对应的格子位置,以及保存和加载功能。
    class Stitch():
        def __init__(self, **kwargs) -> None:
            self.line_normal = kwargs.get('line_normal', 1)
            self.line_thick = kwargs.get('line_thick', 2)
            self.thick_per_count = kwargs.get('thick_per_count', 5)
            self.row = kwargs['row']
            self.col = kwargs['col']
            self.row_size = kwargs['row_size']
            self.col_size = kwargs['col_size']
            self.pad = kwargs['pad']
            self.color_map = kwargs.get('color_map')
            self.generate_cell_map()
            self.update_board_size()
        def update_board_size(self):
            self.h = self.get_board_size(self.row, self.row_size, self.thick_per_count, self.line_normal, self.line_thick)
            self.w = self.get_board_size(self.col, self.col_size, self.thick_per_count, self.line_normal, self.line_thick)
        def save_json(self, file):
            json_dict = {
                "row": self.row,
                "col": self.col,
                "row_size": self.row_size,
                "col_size": self.col_size,
                "pad": self.pad,
                "line_normal": self.line_normal,
                "line_thick": self.line_thick,
                "thick_per_count": self.thick_per_count,
                "color_map": self.color_map
            }
            with open(file, 'w') as f:
                json.dump(json_dict, f)
        @classmethod
        def default(cls):
            json_dict = {
                'line_normal': 1,
                'line_thick': 1,
                'thick_per_count': 5,
                'row': 32,
                'col': 32,
                'row_size': 16,
                'col_size': 16,
                'pad': 32,
                'color_map': {}
            }
            return cls(**json_dict)
        @classmethod
        def from_json(cls, file):
            with open(file, 'r') as f:
                json_dict = json.load(f)
                return cls(**json_dict)
        def generate_cell_map(self):
            self.cell_map = {}
            if self.color_map is None:
                self.color_map = {}
            for color, cells in self.color_map.items():
                for row, col in cells:
                    cell_id = self.get_cell_id(row, col)
                    self.cell_map[cell_id] = color
        def update_color_map(self, row, col, hex_color):
            cell_id = self.get_cell_id(row, col)
            if cell_id is None:
                return
            pt = [row, col]
            prev_color = self.cell_map.get(cell_id, None)
            if prev_color is not None:
                if prev_color in self.color_map:
                    cmap = self.color_map[prev_color]
                    if pt in cmap:
                        cmap.remove(pt)
            if hex_color is not None:
                if hex_color in self.color_map:
                    self.color_map[hex_color].append(pt)
                else:
                    self.color_map[hex_color] = [pt]
            self.cell_map[cell_id] = hex_color
        def get_board_lines(self):
            rows, cols = [], []
            x0, y0 = self.pad, self.pad
            # draw rows
            x1 = x0
            x2 = x0 + self.w - 1
            for i in range(self.row+1):
                n = i + 1
                is_thick = i % self.thick_per_count == 0 or i == self.row
                y = self.get_line_pos(n, self.row_size, self.pad, self.thick_per_count, self.line_normal, self.line_thick)
                rows.append([(x1, y), (x2, y), is_thick])
            # draw cols
            y1 = y0
            y2 = y0 + self.h - 1
            for i in range(self.col+1):
                n = i + 1
                is_thick = i % self.thick_per_count == 0 or i == self.col
                x = self.get_line_pos(n, self.col_size, self.pad, self.thick_per_count, self.line_normal, self.line_thick)
                cols.append([(x, y1), (x, y2), is_thick])
            return rows, cols
        def get_board_str(self):
            rows, cols = [], []
            row_font_size = self.row_size
            col_font_size = self.col_size
            x0, y0 = self.pad, self.pad
            # draw row text
            x1 = x0
            x2 = x0 + self.w - 1
            for i in range(self.row):
                n = i + 1
                if n % self.thick_per_count == 0 or i == self.row:
                    y = self.get_cell_pos(n, self.row_size, self.pad, self.thick_per_count, self.line_normal, self.line_thick)
                    y += (self.row_size + row_font_size) / 2
                    rows.append([x1-row_font_size-2, y, str(n)])
                    rows.append([x2+2, y, str(n)])
            # draw col text
            # draw cols
            y1 = y0
            y2 = y0 + self.h - 1
            for i in range(self.col):
                n = i + 1
                if n % self.thick_per_count == 0 or i == self.col:
                    x = self.get_cell_pos(n, self.col_size, self.pad, self.thick_per_count, self.line_normal, self.line_thick)
                    x += (self.col_size - col_font_size) / 2
                    rows.append([x, y1-2, str(n)])
                    rows.append([x, y2+col_font_size+2, str(n)])
            return rows, cols
        def get_cell_id(self, row, col):
            if row is None or col is None:
                return None
            cell_id = (row - 1) * self.row_size + col
            return cell_id
        def get_cell_qrect(self, row, col):
            rect = self.get_cell_rect(row, col, self.row_size, self.col_size, self.pad, self.thick_per_count, self.line_normal, self.line_thick)
            qrect = [rect[0], rect[1], rect[2]-rect[0]+1, rect[3]-rect[1]+1]
            return qrect
        def get_cell(self, x, y):
            cell_row = self.get_cell_by_pos(y, self.row_size, self.h, self.row)
            cell_col = self.get_cell_by_pos(x, self.col_size, self.w, self.col)
            cell_id = None
            cell_qrect = None
            if cell_row is not None and cell_col is not None:
                cell_qrect = self.get_cell_qrect(cell_row, cell_col)
                cell_id = self.get_cell_id(cell_row, cell_col)
            return cell_row, cell_col, cell_id, cell_qrect
        def get_cell_by_pos(self, pos, size, boarder_size, cell_count):
            pos -= self.pad
            if pos  self.pad + boarder_size:
                return None
            block_size = self.line_thick + (size + self.line_normal) * self.thick_per_count - self.line_normal
            block_i = pos // block_size
            block_bias = pos % block_size
            line_bias = self.line_thick - self.line_normal
            cell_size = self.line_normal + size
            cell_n = (block_bias - line_bias) // cell_size + 1
            cell = block_i * self.thick_per_count + cell_n
            if cell  cell_count:
                cell = None
            return cell
        def save_image(self, file):
            print(f'border size: {(self.w, self.h)}')
            im_w = self.w + self.pad * 2
            im_h = self.h + self.pad * 2
            print(f'canvas size: {(im_w, im_h)}')
            im = Image.new('RGB', size=(im_w, im_h), color='white')
            draw = ImageDraw.Draw(im)
            color_black = "#000000"
            color_gray = "#cccccc"
            x0, y0 = self.pad, self.pad
            draw_later_lines = []
            # draw rows
            x1 = x0
            x2 = x0 + self.w - 1
            for i in range(self.row+1):
                n = i + 1
                y = self.get_line_pos(n, self.row_size, self.pad, self.thick_per_count, self.line_normal, self.line_thick)
                is_thick = i % self.thick_per_count == 0 or i == self.row
                t = self.line_thick if is_thick else self.line_normal
                c = color_black if is_thick else color_gray
                if is_thick:
                    draw_later_lines.append([[(x1, y), (x2, y)], c, t])
                else:
                    draw.line([(x1, y), (x2, y)], c, t)
                    print(f'[row][{n}] pt1: {(x1, y)}, pt2: {(x2, y)}')
            # draw cols
            y1 = y0
            y2 = y0 + self.h - 1
            for i in range(self.col+1):
                n = i + 1
                x = self.get_line_pos(n, self.col_size, self.pad, self.thick_per_count, self.line_normal, self.line_thick)
                is_thick = i % self.thick_per_count == 0 or i == self.col
                t = self.line_thick if is_thick else self.line_normal
                c = color_black if is_thick else color_gray
                if is_thick:
                    draw_later_lines.append([[(x, y1), (x, y2)], c, t])
                else:
                    draw.line([(x, y1), (x, y2)], c, t)
                    print(f'[col][{n}] pt1: {(x, y1)}, pt2: {(x, y1)}')
            if len(draw_later_lines) > 0:
                for i, l in enumerate(draw_later_lines):
                    n = i + 1
                    draw.line(l[0], l[1], l[2])
                    print(f'[thick][{n}] pt1: {l[0][0]}, pt2: {l[0][1]}')
            # row_font = ImageFont.truetype("Ubuntu-M.ttf", self.row_size)
            # col_font = ImageFont.truetype("Ubuntu-M.ttf", self.col_size)
            row_font = ImageFont.truetype("simhei.ttf", self.row_size)
            col_font = ImageFont.truetype("simhei.ttf", self.col_size)
            # draw row text
            for i in range(self.row):
                n = i + 1
                if n % self.thick_per_count == 0 or i == self.row:
                    y = self.get_cell_mid_pos(n, self.row_size, self.pad, self.thick_per_count, self.line_normal, self.line_thick)
                    draw.text((x1-2, y), str(n), anchor="rm", fill=color_black, font=row_font)
                    draw.text((x2+2, y), str(n), anchor="lm", fill=color_black, font=row_font)
            # draw col text
            for i in range(self.col):
                n = i + 1
                if n % self.thick_per_count == 0 or i == self.col:
                    x = self.get_cell_mid_pos(n, self.col_size, self.pad, self.thick_per_count, self.line_normal, self.line_thick)
                    draw.text((x, y1-2), str(n), anchor="md", fill=color_black, font=col_font)
                    draw.text((x, y2+2), str(n), anchor="ma", fill=color_black, font=col_font)
            color_map = self.color_map
            if color_map is not None:
                for color, cells in color_map.items():
                    if not color.startswith('#'):
                        color = '#' + color
                    for cell in cells:
                        rect = self.get_cell_rect(cell[0], cell[1], self.row_size, self.col_size, self.pad, self.thick_per_count, self.line_normal, self.line_thick)
                        draw.rectangle(rect, fill=color, width=0)
            im.save(file)
            print(f'Saved image as "{file}"')
        @classmethod
        def convert_hex_to_rgb(self, hex):
            hex = hex.strip('#').strip()
            if len(hex) != 6:
                return None
            r = int(hex[0:2], 16)
            g = int(hex[2:4], 16)
            b = int(hex[4:6], 16)
            return r, g, b
        @classmethod
        def convert_rgb_to_hex(self, r, g, b):
            color = hex(r)[2:].zfill(2)
            color += hex(g)[2:].zfill(2)
            color += hex(b)[2:].zfill(2)
            return color
        @classmethod
        def get_board_size(cls, n, size, thick_per_count, line_normal, line_thick):
            return n * size + (n + 1) * line_normal + (math.ceil(n / thick_per_count) + 1) * (line_thick - line_normal)
        @classmethod
        def get_line_pos(cls, n, size, pad, thick_per_count, line_normal, line_thick):
            i = n - 1
            return pad + i * size + i * line_normal + math.ceil(i / thick_per_count) * (line_thick - line_normal)
        @classmethod
        def get_cell_pos(cls, n, size, pad, thick_per_count, line_normal, line_thick):
            return pad + (n - 1) * size + n * line_normal + math.ceil(n / thick_per_count) * (line_thick - line_normal)
        @classmethod
        def get_cell_mid_pos(cls, n, size, pad, thick_per_count, line_normal, line_thick):
            x = cls.get_cell_pos(n, size, pad, thick_per_count, line_normal, line_thick)
            return x + math.floor(size / 2) - 1
        @classmethod
        def get_cell_rect(cls, row, col, row_size, col_size, pad, thick_per_count, line_normal, line_thick):
            x1 = cls.get_cell_pos(col, col_size, pad, thick_per_count, line_normal, line_thick)
            y1 = cls.get_cell_pos(row, row_size, pad, thick_per_count, line_normal, line_thick)
            x2 = x1 + col_size - 1
            y2 = y1 + row_size - 1
            return (x1, y1, x2, y2)
    0x4 如何运行
    $ cd draw_stitch_gui
    $ python3 -u draw_stitch_gui.py
    0x5 效果图


    screenshot.png (29.18 KB, 下载次数: 0)
    下载附件
    2024-4-8 16:54 上传

    0x6 完整源码

    用鼠标点出像素画工具.7z
    (61.64 KB, 下载次数: 29)
    2024-4-8 16:56 上传
    点击文件名下载附件
    源代码下载积分: 吾爱币 -1 CB

    解压密码:52pojie.cn

    画工, 像素

  • wkdxz   

    简单实用的软件,谢谢分享!
    atoms   

    楼主这个真不错 之前我知道一个在线画像素图像的 https://www.pixilart.com/draw  虽然功能很强 但是是英文的
    a5323671   

    谢谢分享!
    jiushiyaole   

    很方便的工具
    qhkm99   

    谢谢分享工具
    1e3e   

    谢谢分享呀
    LightswornSnow   

    标序号以前还真没考虑过,挺好的想法,参考一下
    FHWX   

    感谢分享!
    yuzilin   

    感谢分享呀~
    您需要登录后才可以回帖 登录 | 立即注册

    返回顶部