肝个斗图机器人,打败隔壁小胖墩

点击上方 前端瓶子君,关注公众号

回复算法,加入前端编程面试算法每日一题群

肝个斗图机器人,打败隔壁小胖墩_第1张图片

前言

有一天,组织内的斗图机器人坏掉了,巧不巧的是当你需要用它时,它坏掉了。

赶上要催交同学们的周报,没有表情包,就没办法委婉又不礼仪并友好和善的催促同学们交周报。

然后只能自己做图,打开了度娘,找合适表情,然后打开sketch,一通操作后,粘贴到群,搞定。

but总使用同一表情,又很枯燥,于是又打开度娘,打开sketch,一通操作,粘贴到群,搞定。

过了一段时间,度娘,sketch,群。

又过了一段时间,度娘,sketch,群。

时间一长就会很烦躁,每次都要这样搞半天(难道喜新厌旧属性?感觉像渣男?)

后来,突然开朗。求人不如求己!发挥我的主观能动性!自己敲一个!

于是在经历Node的洗礼,Color的洗礼,Canvas的洗礼,SQL的洗礼,Docker的洗礼,Vercel的洗礼后,它诞生了。

它叫imeme,是一个斗图机器人。

本文目的

给大家介绍如何设计和实现一款斗图机器人,是有前端有后端的全栈开发。

不会讲的

  • 涉及到安全问题、隐私以及制度政策等原因,机器人的接收消息内容不介绍

  • 具体功能演示,不提供截图展示,可自行体验

  • 不会详细讲清楚每一个实现细节

But,这些限制要素无关紧要,不影响全局,也不影响大家搭建自己的机器人。

重点讲解

  • 机器人的技术选型

  • 关键环节的设计思路及相关知识点。

场景还原

使用markdown还原下真实交互场景

肝个斗图机器人,打败隔壁小胖墩_第2张图片 image.png

技术选型

明确目标,鼓舞斗志。

那么应该如何设计主体流程?先从最基本的功能入手,列下需求清单:

  • Server,用来接收命令,发送消息。

  • 绘图功能,能够把文字和图片做成一张图。

  • 图片处理,不同的图片类型采取不同策略,获取最基本的图片信息。

  • 数据存储,作为数据源,提供各种有意思的基础图片及与绘图相关的基本参数。

  • 录入导出,便于数据采集,迎支持插入多条数据以及数据库的备份。

  • UI,让imeme用起来更轻松,便于管理数据源,查看图片以及调整绘图参数,还应支持交互式新增和图片下载。

针对如上特点:

  1. Server端,基于express实现node服务,axios + canvas + sql.js。

  2. UI端,vite + vue3.x + typescript设计实现,并提供lib库供多端快捷接入。

整体架构图

简单怼了一张图

肝个斗图机器人,打败隔壁小胖墩_第3张图片 image.png

界面管理就很常见了,大致长这样

肝个斗图机器人,打败隔壁小胖墩_第4张图片 image.png

关键环节的设计思路

所有源码,链接在文末参考资料中,在github上。服务部署到vercel,可访问体验Web端[3](网速不稳定,毕竟白嫖vercel)

Server

Server要实现,接收到消息命令请求后,绘制图形,并能够给出合理结果反馈,也就是新的图像。所以基于express实现node服务,接口的设计要求如下:

  1. /test 用于测试服务的可用性,get请求。

  2. 设置origin * 允许接口的跨域请求以及多种请求头,默认编码utf-8。

  3. 为Chat端提供的/send,post发送Webhooks消息体。

  4. 为Web端提供的/image/*接口

  • /catalog用于目录获取,读取数据库中存储的图片源列表显示。

  • /open 打开用户选中的列表内容,接口返回图片基本信息(base64及绘图数据)。

  • /save 绘图数据的保存接口,用于图片拖拽编辑后,把最新数据同步到数据库中。

  • /create 新建表情,保存到数据库。

  • /update 更新表情数据

  • /download 下载接口,用户拖拽好的内容,可以直接下载到本地。

  • /export 数据导出备份

Server的接口逻辑在service模块,分为四个层次

  • router.js api接口层,管理服务提供的所有api。

  • data.js 连接接口和数据库的数据层,数据封装,为api提供数据获取服务。

  • ajax.js 请求结果集封装,根据data.js请求,给出结果反馈信息。

  • send.jsChat端提供的发送消息服务

绘图

简单的讲,表情就是图片加文字,即我们常见的水印,选择使用canvas来处理。

Node本身不具备canvas的能力,需要借助`canvas库`[4]来实现基本的绘图能力。

本部分内容在convert模块,主要提供给Chat端使用。

Web端不需要这些,对于浏览器来讲,canvas绘图小菜一碟,属于基本操作。

这里按照功能逻辑设计,分为4个层次:

  • make.js 提供绘图能力,支持图片本地保持。

  • size.js 根据base64串获取图片的widht和height。

  • format.js 菜单格式化,无效命令反馈。

  • parser.js 解析接收到的请求命令。

一个完整的水印图,由很多部分组成,拆解为base64编码的图片,水印文字,文字的位置横纵坐标,文字的颜色,字体大小,对齐方向,最大宽度。

绘图,就是把上述已知信息整合到一起

const make = (text, options) => {
  const base64Img = options.image;
  const parts = base64Img.split(';base64,');
  const type = parts[0].split(':').pop();

  if (NOT_SUPPORT.includes(type) || text === '') {
    return base64Img;
  }

  let base64 = '';
  const {width, height} = getSize(base64Img);

  if (width && height) {
    const img = new Image();
    const canvas = createCanvas(width, height);
    const ctx = canvas.getContext('2d');

    img.onload = () => {
      ctx.drawImage(img, 0, 0);

      const {x, y, font, color, align, max} = options;
      ctx.font = font;
      ctx.fillStyle = color;
      ctx.textAlign = align;
      ctx.fillText(text, x, y, max || width);

      base64 = canvas.toDataURL(type);
    };
    img.onerror = err => {
      console.error(err);
    };
    img.src = base64Img;
  }
  return base64;
};
复制代码

首先,根据base64编码,获取图片内容的基本类型,不同类型的图片,需要不同的解析流程。对于暂不支持水印功能的图片格式或者空命令的请求,直接返回base64原始编码。

接下来,调用size.js中的getSize获取图片的width和height,创建固定大小的canvas画布,进一步,得到ctx。

因为水印中图片在下层,文字在上层,所以先通过ctx.drawImage(img, 0, 0)绘制原始图片,再结合ctx.fillText(text, x, y, max || width)在(x, y)点,绘制最大长度为max的文字信息。

最后,通过base64 = canvas.toDataURL(type)生产出我们需要的绘图后的base64编码。

另外,在make.js中还提供了writeImg方法,可用于在开发中及时本地调试位置参数信息,检测生产的图片是否满足要求。(已经提供UI的交互式调整,解放了本地调试的痛苦)

图片尺寸

这部分内容在size.js,原理是根据base64的buffer,提取image的width和height。

针对不同格式的图片,要采取不同的处理策略,imeme目前提供5种(png/jpg/jpeg/gif/bpm)图片格式的处理,我们以png为例来说明,如何根据图片的buffer获取,真实的尺寸。

这里,你需要一点点的node buffer知识,以及了解简单的图片编码原理。

每种类型的文件都有自己独特的标识,直观上通过文件的扩展名来区分类型,然而扩展名可以随意的更改。所有的文件在计算机上都是以二进制方式存储的,我们可以通过分析标识头来确定文件类型。

我们本地查看任意一个png文件,用十六进制编辑器打开(可使用vscode的hexdump[5]

肝个斗图机器人,打败隔壁小胖墩_第5张图片 image.png

我们分析下前两行内容

  • 89 50 4E 47 0D 0A 1A 0A png文件的标识头

  • 00 00 00 0D IHDR头块长度为13 bytes

  • 49 48 44 52 IHDR标识

  • 00 00 00 BC width,换算成十进制为188(16 * 11 + 12)px

  • 00 00 00 C4 height,换算成十进制为196(16 * 12 + 4)px

  • 08 色深,换算下即2^8=256,即256色的图像

  • 06 颜色类型,6表示,带α通道数据的真彩色图像

  • 00 压缩方法,LZ77派生算法(PNG Spec规定此处总为0,非0值为将来使用更好的压缩方法预留)

  • 00 滤波器方法,总为0,同上

  • 00 隔行扫描方法,0表示采用非隔行扫描

  • 25 38 3B 07 4个byte的CRC校验[6]

肝个斗图机器人,打败隔壁小胖墩_第6张图片 image.png

在MacOS可以通过file快速查看1.png

$ file 1.png 
1.png: PNG image data, 188 x 196, 8-bit/color RGBA, non-interlaced
复制代码
  • width位于第16个byte,长度是4bytes

  • height位于第20个byte,长度是4bytes

const getPNGSize = buffer => {
  let w = 16;
  let h = 20;
  return {
    width: buffer.readUInt32BE(w),
    height: buffer.readUInt32BE(h)
  };
};
复制代码

buffer又是什么?

我们简化一下base64图片格式,还是以png为例讲解

data:image/png;base64,CODE
复制代码

对base64编码的图片字符串,解析,获取到CODE内容,然后使用Buffer.from转换为'base64'编码的buffer

import {Buffer} from 'buffer';

const buffer = Buffer.from(CODE.toString(), 'base64');
复制代码

vscode还可以使用Hex Editor[7]插件,能够更快捷的查看转码后的内容,同时也能够帮助buffer的转换提供一些思路。hexdump[8]需要鼠标hover才会提示。

肝个斗图机器人,打败隔壁小胖墩_第7张图片 image.png

其他图片格式,同理可得!!(???说的好轻松???)

例如gif文件

肝个斗图机器人,打败隔壁小胖墩_第8张图片 image.png
const getGIFSize = buffer => {
  return {
    width: buffer.readUInt16LE(6),
    height: buffer.readUInt16LE(8)
  };
};
复制代码
f4fa172f32ef763d6a142c4d21884327.png image.png

DB

数据存储,使用SQLite,足够轻量,简单易学易用,需要引入`sql.js`[9]

功能介绍

该部分在db模块,基本涵盖的功能可以概括为:

  • 数据库的初始化、读取、存储、重置

  • 数据表的初始化、查询、插入、更新和删除

  • 获取某表的一条数据

  • 获取某表的所有数据

  • 获取所有数据

  • 日志

表结构设计

表结构,目前设计了四张表

  • STORY 记录图片指令和base64的image

  • TEXT 记录图片对应的绘图信息,例如x, y, font, color等

  • LOGGER 日志表,主要收集imeme缺失的资源

  • SPECIAL 特殊表,表结构同STORY,用于保存彩蛋指令,像中秋节、国庆节这种关键字,Chat端通过@imeme是查询不到的,属于隐藏的key,使用@imeme 中秋 金馆长会随机返回一张图。

CREATE TABLE STORY (
  id INTEGER PRIMARY KEY AUTOINCREMENT,
  mid CHAR(50) NOT NULL,
  title CHAR(100) COLLATE NOCASE,
  feature CHAR(100) COLLATE NOCASE,
  image TEXT NOT NULL
)
复制代码
  • id 主键,自增,不用于其他操作

  • mid 唯一key,用于数据的各种操作

  • title 文件标题,图片指令,唯一

  • feature 所属类别,用于归类,很多title可以对应同一个feature

  • image base64 image

不同于MYSQL,由于SQLite是大小写不敏感的数据库,所以为省去后面使用上的麻烦,建表的时候,把所有字段都统一小写。

数据备份

灾备的话,目前仅提供基于脚本的方式备份数据,npm run backup,默认把常用表和特殊表的内容,转化成js文件,存储到指定位置,默认为assets/backup目录。(后续会支持数据库自动备份)

数据采集

提供两种方式的数据导入

  • npm run import fileName默认读取assets/fileName目录,获取满足格式要求的文件,转换为base64,并附加绘图基本信息,存储为fileName.js文件。

  • 交互式添加单个图片,自定义表情内容,支持选择、拖拽以及拷贝粘贴的方式添加新图。

图形界面

为了更加友好方便快捷的斗图,imeme需要配备一个管理端imeme-view,它主要做这些事:

  • 管理数据源,管理imeme所有的表情资源

  • 查看表情

  • 动态调整绘图参数,支持可拖拽本文编辑,实时查看

  • 新增表情,提供选择框,拖拽、拷贝粘贴三种方式导入

  • 下载,实时下载表情资源

肝个斗图机器人,打败隔壁小胖墩_第9张图片 image.png
部署

前端静态页依赖于Gitbhu Action[10]托管在Github Pages[11],Node Server部署在Vercel[12]

vue3 + vite

你可能感兴趣的:(python,java,数据库,大数据,编程语言)