MiraiForum

    • 注册
    • 登录
    • 搜索
    • 热门
    • 最新
    • 未解决
    • 标签
    • 群组
    • 友情链接
    1. 主页
    2. Dituon
    3. 最佳
    • 资料
    • 关注 0
    • 粉丝 7
    • 主题 12
    • 帖子 285
    • 最佳 49
    • 有争议的 0
    • 群组 1

    Dituon 发布的最佳帖子

    • Petpet - 生成各种奇怪的图片

      Petpet - 生成各种奇怪的图片

      已更新 6.2 版本, 支持多线程, 可自定义 头像 文字 坐标 概率 关键词 频率限制 等参数


      项目地址

      releases

      前端版本 / 功能预览


      如果你觉得此插件对你有帮助, 请在此帖下回复不错的插件 让更多人看到, 这对我非常重要!


      Petpet

      Mirai version
      GitHub
      GitHub all releases
      GitHub Repo stars
      GitHub release (latest by date)
      GitHub issues
      GitHub closed issues
      GitHub closed pull requests

      自定义合成图片的 Mirai 插件 / 独立程序 / gocq-http插件, 灵感/部分数据来自 nonebot-plugin-petpet。

      原生 java 编写, kotlin仅用于数据序列化, 使用底层API, 多线程优化: 轻量, 高性能, 易拓展

      • 在线编辑器

      • JS 前端版本

      • 在线体验

      使用方法

      单独运行

      1. 下载 最新版本 petpet.jar 或 petpet-no-ws.jar

      2. 下载 图片素材

      3. 将图片素材放入 ./data/xmmt.dituon.petpet/ 目录

      4. 运行 start.bat 或 start.sh, 可自行更改配置文件 config.json, 重启后生效

      5. 参考WebServer一节 发起网络请求 / 或使用WebUI

      Mirai插件

      1. 部署 Mirai 机器人框架

      2. 下载 最新版本

      3. 将插件放入 Mirai/plugins/

      4. 下载 图片素材

      5. 将图片素材放入 Mirai/data/xmmt.dituon.petpet/

      6. 启动 Mirai, 可自行更改配置文件 Petpet.yml, 重启后生效 (参考 配置项说明)

      • 使用 戳一戳 有 30% 的概率触发; 或发送 pet @xxx

      pet key @xxx 或 key @xxx 可返回指定图片 例如 pet kiss @xxx kiss @xxx

      可通过发送的图片生成Petpet kiss [图片], 支持GIF

      可通过回复构造图片, 例如 [图片] -> [回复[图片]] 对称

      可使用 pet指令 获取 keyList

      gocq-http插件

      Warning

      此功能处于测试阶段, 目前仅能通过key生成图片, 请期待后续开发!

      1. 部署 gocq-http 机器人框架, 设置正向 WebSocket 监听 (默认端口为8080)

      2. 更改 gocq-http 配置项 message.post-format 为 array

      3. 下载 最新版本 petpet.jar

      4. 下载 图片素材

      5. 将图片素材放入 ./data/xmmt.dituon.petpet/ 目录

      6. cd ./ java -jar petpet.jar -gocq, 可自行更改配置文件 gocq-config.json, 重启后生效

      配置文件

      配置项说明

      <details>

      <summary>展开/收起</summary>
      <br/>

      • command: pet

      触发petpet指令, 默认为pet

      例: pet @xxx pet kiss @xxx

      仅发送pet时会返回keyList
      <br/>

      • probability: 30

      戳一戳 触发概率, 0-100整数, 默认为 30%
      <br/>

      • antialias: true

      画布抗锯齿, 默认为true
      <br/>

      • resampling: true

      重采样缩放, 启用后头像质量更高, 可对模板单独配置
      <br/>

      • disabled: []

      禁用表列, 默认为空, 在此数组中的key不会被随机触发 (会覆盖data.json中的配置)
      <br/>

      • keyCommandHead: ''

      key作为指令头时的前缀, 默认为空

      例 (配置项为'#'时): #kiss @xxx osu hso!
      <br/>

      • respondReply: true

      响应回复的消息, 默认为true

      可通过回复消息 定位到之前发送的图片并构造petpet

      启用后 会缓存接收到的图片(见cachePoolSize)

      例 : [回复[图片]]kiss(等价于 kiss [图片])
      <br/>

      • cachePoolSize: 10000

      respondReply=true时, 图片消息缓存池大小, 默认为10000

      本质为HashMap<imageId(long), imageUrl(String)>, 超过此限制会清空Map
      <br/>

      • respondSelfNudge: false

      某些情况下, 机器人会主动戳其他成员, 响应机器人自己发出的戳一戳, 默认为false
      <br/>

      • keyListFormat: FORWARD

      发送pet时 keyList响应格式, 默认为FORWARD

      枚举: MESSAGE(发送普通消息) FORWARD(发送转发消息) IMAGE(发送图片)
      <br/>

      • disablePolicy: FULL

      发送pet on/off时 禁用哪些功能, 默认为FULL

      枚举: NONE(无效) NUDGE(只禁用戳一戳) MESSAGE(只禁用指令) FULL(同时禁用戳一戳和指令)

      • fuzzy: false

      模糊匹配用户名, 默认为false

      例 : (配置项为true时): kiss @田所浩二(响应) kiss 浩二(响应)
      <br/>

      • strictCommand: true

      严格匹配指令, 默认为true

      人话: 可以省略key后的空格

      例 : (配置项为false时): kiss 田所(响应) kiss田所(响应)
      <br/>

      • synchronized: false

      消息事件同步锁, 会锁住相同的消息事件, 默认为false

      人话: 多机器人对于同一条指令只有一个会响应
      <br/>

      • gifEncoder: ANIMATED_LIB

      GIF编码器, 默认为ANIMATED_LIB

      枚举:
      BUFFERED_STREAM:
      基于缓存的STREAM流, 在编码过程中对Gif进行压缩;

      • 编码速度较慢, 所需堆内存小, 生成Gif体积小

      ANIMATED_LIB:
      基于byte[]序列, 使用多线程分析像素;

      • 编码速度极快, 所需堆内存较多, 生成Gif体积较小

      <br/>

      • gifMaxSize: []

      GIF缩放阈值/尺寸, 默认为空 (不限制)

      [width, height, frameLength]:

      当Gif长度超过frameLength时, 会对Gif进行等比例缩放

      注: 缩放在图片合成时进行, 不会影响性能

      例: (配置项为[200, 200, 32]时)

      • 当Gif长度超过32帧时, 检查Gif尺寸
      • 当Gif尺寸大于200*200时, 对Gif进行等比例缩放
      • Gif缩放后 最长边不会超过设定值
        (当Gif中包含40帧, 尺寸为300*500时)
      • 输出的Gif长度不变, 尺寸为120*200
      • gifQuality: 5

      Gif编码质量(1-49), 默认为5

      数字越小, 速度越慢, 质量越好 (大于20时, 速度不会有明显提升)

      仅适用于ANIMATED_LIB编码器

      • headless: true

      启用headless模式, 默认为true

      人话: 有些服务器没有输入输出设备, 画图库无法正常运行, 启用这个配置项可以修复, 因为总是有人不看常见问题, 干脆默认启用了(
      <br/>

      • autoUpdate: true

      自动更新PetData, 每次启动时都会检查并自动下载船新pet, 默认为true

      注: 仅更新PetData, 不会更新插件版本, 请放心食用

      人话: 每次启动都会自动下载新的超赞梗图, 墙裂推荐
      <br/>

      • repositoryUrl: 'https://dituon.github.io/petpet'

      仓库地址, 用于自动更新, 默认为此仓库的github page

      • devMode: false

      开发模式, 启用后任何人都能使用pet reload指令热重载PetData, 默认为false
      <br/>

      • messageHook: false

      消息注入, 参考MessageHook, 默认为false
      <br/>

      • coolDown: 1000

      成功触发指令后对该用户的冷却时间(单位为毫秒), 默认为 1000

      设置为 -1 可禁用冷却
      <br/>

      • groupCoolDown: -1

      成功触发指令后对该群聊的冷却时间, 默认为 -1
      <br/>

      • inCoolDownMessage: 技能冷却中...

      在冷却时间中触发命令的回复消息

      配置项为[nudge]时, 会以戳一戳形式回复
      <br/>

      </details>

      修改后重启 Mirai 以重新加载

      权限管理

      群主或管理员使用 pet on pet off 以 启用/禁用 戳一戳

      pet on/off指令控制的事件可在配置文件中更改

      可在配置文件中禁用指定key, 被禁用的key不会随机触发, 但仍可以通过指令使用

      图片预览

      在线尝试

      自定义

      在线编辑器

      data.json

      ./data/xmmt.dituon.petpet/ 下的目录名为 key ,插件启动时会遍历 ./data/xmmt.dituon.petpet/$key/data.json

      data.json 是模板配置文件, 程序解析此文件以生成图像

      {
        "type": "GIF",
        "avatar": [],
        "text": [],
        "delay": 50,
        "alias": [ "别名1", "别名2" ]
      }
      
      属性 类型 注释 默认值
      type 模板类型枚举 图片类型枚举, IMG或GIF 必须
      avatar Avatar 数组 头像配置数组, 见下文 必须
      text Text 数组 文本配置数组, 见下文 必须
      inRandomList 布尔值 是否在随机列表中 false
      reverse 布尔值 GIF是否倒放 false
      delay 整数 帧间延时 (毫秒) 65
      background Background 背景配置, 见下文 null
      alias 字符串数组 别名数组 []
      hidden 布尔值 是否隐藏 false
      模板类型枚举
      • GIF 动图, 程序会读取目录下所有.png格式的图像
      • IMG 静态图片, 程序会读取目录下随机.png格式的图像

      头像

      程序支持复杂的图像处理, 包括裁切, 旋转, 透明度, 滤镜等

      {
        "avatar": [
          {
            "type": "FROM",
            "pos": [[92, 64, 40, 40], [135, 40, 40, 40], [84, 105, 40, 40]],
            "round": true,
            "rotate": false,
            "avatarOnTop": true,
            "angle": 90
          },
          {
            "type": "TO",
            "pos": [[5, 8], [60, 90], [50, 90], [50, 0], [60, 120]],
            "posType": "DEFORM",
            "opacity": 0.5
          }
        ]
      }
      
      属性 类型 注释 默认值
      type 头像类型枚举 见下文, 例如FROM或TO 必须
      pos 坐标数组 头像的坐标信息 必须
      posType 坐标格式枚举 坐标格式枚举, ZOOM或DEFORM ZOOM
      round 布尔值 头像是否裁切为圆形 false
      avatarOnTop 布尔值 头像图层是否在背景之上 true
      angle 整数 头像的初始角度 0
      origin 旋转原点枚举 头像的旋转原点 DEFAULT
      opacity 浮点数 头像的不透明度 1.0
      rotate 布尔值 GIF类型的头像是否旋转 false
      fit 填充模式枚举 填充模式枚举, 可以是CONTAIN或FILL FILL
      crop 裁切坐标数组 头像裁切坐标信息 null
      cropType 裁切格式枚举 见下文 NONE
      style 风格化枚举数组 风格化枚举数组, 见下文 []
      filter 滤镜对象数组 滤镜数组, 见下文 []
      antialias 布尔值 是否使用抗锯齿算法, 默认跟随全局配置 null
      resampling 布尔值 是否使用重采样缩放, 默认跟随全局配置 null

      头像类型枚举 type

      • FROM 发送者头像
      • TO 接收者头像, 或构造的图片
      • GROUP 群头像
      • BOT 机器人头像
      • RANDOM 随机头像 (随机从群聊成员中选择, 不会重复)

      坐标

      坐标格式枚举posType

      • ZOOM 缩放, 通过 x, y, width, height 表示图像
      • DEFORM 变形, 通过四点坐标来表示图像
      ZOOM

      ZOOM 缩放坐标的基本组成单位是 4长度 int[] 数组

      其中,前两项为 左上角顶点坐标, 后两项为 宽度和高度

      例:
      [65, 128, 77, 72] 即 头像的左上角顶点坐标是 (65,128), 宽度为 77, 高度为 72

      如果是 GIF 类型,坐标应为二维数组,GIF 的每一帧视为单个图像文件

       {
        // pos的元素对应GIF的4帧
        "pos": [[65, 128, 77, 72], [67, 128, 73, 72], [54, 139, 94, 61], [57, 135, 86, 65]]
      }
      

      如果是IMG类型, 可以使用一维数组

      {
        "pos": [0, 0, 200, 200]
      }
      

      坐标支持变量运算, 例如 [100,100,"width/2","height*1.5^2"]

      坐标变量

      • width 原图宽度
      • height 原图高度
      DEFORM

      DEFORM 仿射变换坐标格式为 [[x1,y1],[x2,y2],[x3,y3],[x4,y4],[x_anchor,y_anchor]];
      分别对应图片的[[左上角],[左下角],[右下角],[右上角],[锚点]],四角坐标用相对于锚点的偏移量表示

      旋转原点枚举 origin

      • DEFAULT 左上角
      • CENTER 中心

      裁切

      图片裁切坐标 [x1, y1, x2, y2], [0, 0, x2, y2] 可简写为 [x2, y2]

      裁切格式枚举 cropType

      • NONE 不裁切
      • PIXEL 按像素裁切
      • PERCENT 按百分比裁切

      填充模式 fit

      • CONTAIN 缩小以适应画布, 不改变原比例
      • COVER 裁切以适应画布, 不改变原比例
      • FILL 拉伸, 改变原比例

      风格化枚举 style

      • MIRROR 水平镜像
      • FLIP 上下翻转
      • GRAY 灰度化
      • BINARIZATION 二值化

      滤镜 filter

      通过滤镜实现头像特效, Java 与 JavaScript 版本实现有偏差, 并非完全相同

      {
        "filter": [
          {
            "type": "SWIRL",
            "radius": 200,
            "angle": 5.0
          }
        ]
      }
      
      滤镜类型 type
      • SWIRL 对应 AvatarSwirlFilter
      • BULGE 对应 AvatarBulgeFilter
      • BLUR 对应 AvatarBlurFilter
      • CONTRAST 对应 AvatarContrastFilter
      • HSB 对应 AvatarHSBFilter
      • HALFTONE 对应 AvatarHalftoneFilter
      • DOT_SCREEN 对应 AvatarDotScreenFilter
      • NOISE 对应 AvatarNoiseFilter
      • DENOISE 对应 AvatarDenoiseFilter
      滤镜对象
      • AvatarSwirlFilter 对象, 漩涡滤镜
      属性 类型 注释 默认值
      radius 浮点数 涡旋半径, 值为0时表示图片半径 0.0
      angle 浮点数 涡旋角度 3.0
      x 浮点数 中心点X坐标百分比 0.5
      y 浮点数 中心点Y坐标百分比 0.5
      • AvatarBulgeFilter 对象, 膨胀收缩滤镜
      属性 类型 注释 默认值
      radius 浮点数 膨胀半径, 值为0时表示图片半径 0.0
      strength 浮点数 膨胀强度 [-1, 1], 负数时产生收缩效果 0.5
      x 浮点数 中心点X坐标百分比 0.5
      y 浮点数 中心点Y坐标百分比 0.5
      • AvatarBlurFilter 对象, 模糊滤镜
      属性 类型 注释 默认值
      radius 浮点数 模糊半径 10.0
      • AvatarContrastFilter 对象, 亮度对比度滤镜
      属性 类型 注释 默认值
      brightness 浮点数 亮度 0.0
      contrast 浮点数 对比度 0.0
      • AvatarHSBFilter 对象, 相对HSB (色相, 饱和度, 亮度) 滤镜
      属性 类型 注释 默认值
      hue 浮点数 色相 0.0
      saturation 浮点数 饱和度 0.0
      brightness 浮点数 亮度 0.0
      • AvatarHalftoneFilter 对象, 半色调滤镜 (模仿彩色印刷的CMYK色彩)
      属性 类型 注释 默认值
      angle 浮点数 角度 0.0
      radius 浮点数 半径 4.0
      x 浮点数 中心点X坐标百分比 0.5
      y 浮点数 中心点Y坐标百分比 0.5
      • AvatarDotScreenFilter 对象, 单色点阵滤镜 (模仿黑白印刷品)
      属性 类型 注释 默认值
      angle 浮点数 角度 0.0
      radius 浮点数 半径 4.0
      x 浮点数 中心点X坐标百分比 0.5
      y 浮点数 中心点Y坐标百分比 0.5
      • AvatarNoiseFilter 对象, 噪声滤镜
      属性 类型 注释 默认值
      amount 浮点数 噪声强度 0.25
      • AvatarDenoiseFilter 对象, 降噪滤镜
      属性 类型 注释 默认值
      exponent 短整数 指数 20

      文字

      如果你想在图片上添加文字,可以编辑 text

      {
        "text": [
          {
            "text": "Petpet!", // 文字内容
            "color": "#66ccff", // 颜色, 默认为#191919
            "pos": [100, 100], // 坐标
            "size": 24 // 字号, 默认为12
          },
          {
            "text": "发送者: $from, 接收者: $to", // 支持变量
            "pos": [20, 150], // 坐标
            "position": ["CENTER", "BOTTOM"], //坐标计算基准([x, y])
            "font": "宋体", // 字体, 默认为黑体
            "strokeColor": "#ffffff", // 描边颜色
            "strokeSize": 2 // 描边宽度
          },
          {
            "text": "$txt1[我]超市$txt2[你]!", // 支持关键词变量
            "pos": [0,200,300], // 第三个值为文本最大宽度
            "align": "CENTER", // 对齐方式, 默认为LEFT
            "wrap": "ZOOM", // 显示设置, 默认为NONE
            "style": "BOLD" // 字体样式, 默认为PLAIN
          }
        ]
      }
      
      属性 类型 注释 默认值
      text 字符串 文本内容 必须
      pos 数组 文本的坐标信息 必须
      color 字符串 文本颜色 #191919
      size 整数 文本字号 12
      angle 整数 头像的初始角度 0
      origin 旋转原点枚举 文字的旋转原点 DEFAULT
      position 数组 文本坐标计算基准 [LEFT, TOP]
      font 字符串 字体 黑体
      strokeColor 字符串 文本描边颜色 null
      strokeSize 整数 文本描边宽度 0
      align 字符串 文本对齐方式 LEFT
      wrap 字符串 文本显示设置 NONE
      style 字符串 字体样式 PLAIN
      greedy 布尔值 是否贪婪匹配多余的关键词 false

      变量

      • $from : 发送者, 会被替换为发送者昵称
      • $to : 接收者, 被戳或At的对象
      • $group : 群名称
      • $txt(i)[(xxx)] : 文本变量, 可用于生成meme图, i为关键词索引, xxx为默认值; 例: $txt1[我]超市$txt2[你] 指令为 pet [key] 我 你

      font

      在data/fonts目录下的字体文件会注册到环境中

      align

      • LEFT: 左对齐, 文本基线是标准的字母基线
      • RIGHT: 右对齐, 文本基线是标准的字母基线
      • CENTER: 居中对齐, 文本基线在文本块的中间

      wrap

      • NONE: 不换行
      • BREAK: 自动换行, 超过最大宽度的文本会显示在下一行
      • ZOOM: 自动缩放, 缩放字体大小以填充最大宽度

      使用BREAK或ZOOM时, maxWidth 默认为 200

      style

      • PLAIN: 默认
      • BOLD: 粗体
      • ITALIC: 斜体
      • BOLD_ITALIC: 粗体与斜体

      position

      • LEFT: 左定位(默认)
      • RIGHT: 右定位
      • TOP: 上定位(默认)
      • BOTTOM: 下定位
      • CENTER: 居中定位

      background

      程序支持动态创建画布

      {
        "background": {
          "size": ["avatar0Width*2","avatar0Height"], //支持变量运算
          "color": "#f0f0f0"
        }
      }
      

      坐标变量

      • avatar(i)Width i号头像(i为定义头像时的顺序, 从0开始)处理后的宽度
      • avatar(i)Height i号头像处理后的高度
      • text(i)Width i号文本渲染后的宽度
      • text(i)Height i号文本渲染后的高度

      MessageHook

      本特性仅适用于 Mirai 插件, 消息注入, 插件会检查将要发送的消息 解析后注入图片, 可配合各类消息回复插件使用

      <pet></pet> 标签中的JSON会被解析, 请求格式参考 WebServer.POST

      用例:

      这段文字之后的标签会变成一张图片发送<pet>{
        "key": "petpet",
        "to": {
          "qq": 2544193782 
        },
        "textList": [
          "text1"
        ]
      }</pet>消息的顺序会被正确处理, 支持多张图片
      

      不同于 POST 请求格式, 你可以用 "qq" 令程序自动获取头像和昵称, 也可以自定义"name" "avatar"
      (更推荐自定义的做法, 程序可能在某些情况下无法推断出正确的"name")

      被"hidden": true隐藏的模板会正常调用

      此功能默认禁用, 需在配置文件中启用messageHook: true

      WebServer

      程序可作为http服务器 / API单独运行, 被其它项目/语言使用

      java -jar petpet.jar

      启动时会生成 config.json:

      {
          "port": 2333, // 监听端口
          "webServerThreadPoolSize": 10, // HTTP服务器线程池容量
          "dataPath": "data/xmmt.dituon.petpet", // PetData路径
          "preview": false, // 启用动态预览 (启动时生成所有模板预览)
          "antialias": true, // 启用抗锯齿, 详见上文
          "resampling": true, // 启用重采样, 详见上文
          "gifMaxSize": [200, 200, 32], // GIF缩放阈值, 详见上文
          "gifEncoder": "ANIMATED_LIB", // GIF编码器, 详见上文
          "gifQuality": 5, // GIF质量, 详见上文
          "threadPoolSize": 0, // GIF编码器线程池容量, 详见上文
          "headless": true // 使用headless模式
      }
      

      程序使用com.sun.net.httpserver实现http服务器

      PetServer API

      访问 127.0.0.1:2333/petpet 以获取 PetDataList

      GET

      使用 GET 传递参数, 例如 127.0.0.1:2333/petpet?key=petpet&toAvatar=$avatarUrl
      127.0.0.1:2333/petpet?key=osu&textList=hso!

      结构
      <details>
      <summary>展开/收起</summary>

      • key (str): 对应PetData,例如kiss rub
      • fromAvatar toAvatar groupAvatar botAvatar (url): 头像URL地址, encodeURIComponent(rawUrl)
      • randomAvatarList (url[]): 随机头像列表, 使用,分割多个url
      • fromName toName groupName (str): 昵称, 有默认值
      • textList (str): 根据空格分割此字符串, 作为额外数据
        </details>

      POST

      使用 POST 传递参数, 例如 127.0.0.1:2333/petpet

      {
          "key": "petpet",
          "to": {
              "name":"d2n",
              "avatar":"https://q1.qlogo.cn/g?b=qq&nk=2544193782&s=640"
          },
          "randomAvatarList": [
              "url"
          ],
          "textList": [
              "text"
          ]
      }
      

      其中, key为必须项, 其它可以省略

      form-data

      可直接将图片二进制文件上传至服务器进行处理

      类似于 GET数据结构, 使用 multipart/form-data

      可参考example-script中的代码实现请求

      语言 示例
      javascript post.js get.js
      python example.py
      php example.php

      WebUI

      启动WebServer后即可使用WebUI

      启用preview配置项以加载WebUI模板预览 (可选, 默认关闭)

      • 修改 server-config.json preview: true

      常见问题

      • 戳一戳无法触发?

        检查 Mirai 登录协议, 仅 ANDORID_PHONE 可以收到 戳一戳 消息

      • 没有生成配置文件?

        Mirai 2.11.0 提供了新的 JavaAutoSaveConfig 方法, 请更新Mirai版本至 2.11.0 (不是2.11.0-M1), 旧版本不支持自定义配置项

      • Could not initialize class java.awt.Toolkit?

        对于无输入输出设备的服务器 需要启用headless

      • 自动更新下载速度慢 / 无法连接远程资源?

        修改Petpet.yml中repositoryUrl的值为'https://ghproxy.com/https://raw.githubusercontent.com/Dituon/petpet/main'(高速镜像)

      • 自动更新后 读取data.json出错?

        自动更新时网络出错导致, 删除出错的文件 重新获取即可

      • 其它错误? 问题?

        若此文档无法解决您的问题, 欢迎提交issue

      性能 & 兼容性

      程序使用底层java.awt类合成图片, 渲染时使用多线程, 静态图片渲染时间一般不会超过1ms

      对GIF编码器的分析, 转换, 映射部分进行多线程优化, 速度极快

      Android JVM没有实现java.awt, 推荐使用JDK 11+版本

      分享你的作品 (模板)

      如果你想分享自定义的 Petpet, 欢迎Pr

      二次开发

      程序提供超多实用API 拓展性极强, 附有互动式开发实例, 欢迎初学者学习!

      • 互动式开发实例 参见test.moe.dituon.petpet.example.HelloPetpet

      • 在别的项目二次开发: mirai-simplepetpet-plugin

      后话

      如果此插件和您预期的一样正常工作,请给我一个 star

      欢迎提交任何请求

      交流群: 922959760

      发布在 插件发布
      Dituon
      Dituon
    • 腾讯官方Bot接口扫盲

      最后更新日期 2024/10/16 请关注最新回复

      注意

      由于楼主文化造纸不高,文中掺杂了很多废话,只阅读粗体字即可了解全部信息,细体字为背景信息补充。

      前言

      众所周知,腾讯几年前为了对标discord和一众竞品推出了秋秋频道,为了对标其它平台的Bot生态开放了仅限频道的官方接口,在开发频道Bot接口时顺便开发了群聊接口,不过仅限内部使用。

      不久之后,腾讯官方的群聊Bot陆续上线,例如群聊管家,农药战绩Bot等;这些用新接口的Bot与腾讯之前与微软合作的QQ小冰,BabyQ等烂尾项目最大的区别是带有权限管理系统,例如群主可以指定Bot权限等。这是因为腾讯把内部接口开放给了缴纳保护费的公司开发对应游戏的战绩查询Bot。

      经过漫长的便秘开发周期,腾讯终于在去年 (2023) 9月宣布对个人开发者开放群聊接口,代价是需要参加腾讯组织的比赛。

      最近腾讯宣布比赛马上结束,现有的接口也基本稳定下来,所以就有了这篇文章。

      资质 & 注册

      注册成为个人开发者,只需要绑定个人身份信息的QQ账户即可;因为几年前的相关部门规定,反而很难找到没有绑定个人信息的账号。

      注册网站: q.qq.com

      注册后即可创建Bot,默认创建的Bot只有频道接口的开发权限,群聊权限会在近期开放申请,请期待本贴更新。

      审核

      接下来是大部分人关心的审核问题,Bot默认只能在测试频道/群聊中使用,添加到其它频道需要通过审核。

      下面的内容包含大量黑框框 你知道的太多了

      审核流程

      1. 平台上上传Bot头像, 指令等基本信息。

      2. 填写excel表格,包含bot的功能、用途、测试用例等信息

        表格预览(可能近期失效,下方有截图)

        e92f20ce-1ffc-4439-80a3-601457c853f8-image.png

      3. 等待过审 (三天到一周时间,周末放假)

      4. 更改Bot使用范围为公域 (下文附有腾讯黑话&专有名词解释)

      5. 等待过审 (两天到一周时间,周末放假)

      (令人迷惑的是为什么腾讯一定要审核两遍,咱也不敢问)

      审核时审核员会根据表格里的指令进行测试
      也不一定按照表格里的指令测试,笔者就遇到过审核员没有按照表格内对应的格式回复导致审核不通过再等一周
      还会假设bot有AI机能尝试聊天

      审核尺度

      建议: 送审时尽可能阉割以免不通过再等一周,过审后可以随意编辑

      文本

      腾讯对文本审核尺度把握非常迷惑严格,列举几个一般人觉得没问题但腾讯不允许出现的词汇:

      上瘾,生病,胸,腿(等身体部位),美国(或任何国家),迪士尼系列的所有名词(米奇,某只黄色的熊等)(任天堂没试过),警察,药

      上面的词汇仅限中文,用英文可以过审

      能否过审主要取决于审核的心情,每个审核的G点不一样

      图片

      想象一下你的祖母会如何评价Bot发送的图片,这就是腾讯的图片审核标准没有标准

      封号

      过审并不是腾讯拷打你的终点,而是起点,以下列举几个可能导致封号的行为

      1. 复读用户发过的消息或发送用户的ID。

      2. 复读用户的表情包/图片或处理后发送用户的头像。

      3. 在某些日子发送或回复某些词汇,强烈建议了解以避免踩雷。坦克,蛋炒饭等

      4. 被举报包含AI回复的功能,AI必须有对应资质。

      接口 & SDK

      接口架构

      目前接口有两个大版本,旧版本只能发送频道,新版本群聊和频道皆可

      websocket 用于事件通知与简单消息发送

      也有http消息发送接口,例如使用formdata发送图片等

      官方SDK

      重要建议:不要使用腾讯官方 Go Node SDK!

      重要建议:不要使用腾讯官方 Go Node SDK!

      重要建议:不要使用腾讯官方 Go Node SDK!

      有Python, Go, Node版本;不建议使用 Go Node,理由如下:

      1. 使用旧版接口

      2. 缺少类型 (TS类型全是any之类的)

      3. 太长时间没更新 (截止到本文撰写,Go与Node SDK上次更新时间是2020年)

      笔者没用过 Python SDK,所以持中立态度。

      野生SDK

      有很多,但笔者一个都没用过,体验过的欢迎评论点评,本贴也会积极更新。

      Markdown & 按钮

      大家朝思暮想的md腾讯也有提供,不过目前限制极大(下详)

      腾讯把Markdown分为三种

      专有名词 门槛 功能 审核/限制
      主动 无 根据预先审核过的模板填充变量发送 每月只能发送4条, 模板需要通过审核(每月中旬集中审核)
      被动 每日用户 >= 2000 根据预先审核过的模板填充变量发送 回复用户的指令, 模板需要通过审核(每月中旬集中审核)
      原生 每日用户 >= 10000 可直接发送md格式的消息 无

      限制

      此外,腾讯还制定了一系列匪夷所思的规则,笔者挑选了几个具有代表性的罄竹难书

      1. URL域名必须备案且经过认证(认证方式是在域名的某个路径放置一个文件)

      2. 服务器必须有公网IP(最近突然发公告说Bot必须绑定IP,被骂的挺惨的,腾讯说会考虑取消这个限制 只是套话 应该不会改)

      3. 不能获取用户信息(无法获取用户QQ号,昵称,头像等,取而代之的是一长串openid,即使是同一用户在不同群也会有不同的ID,腾讯半年前说会更新)

      黑话解释

      列举一下文档里经常出现的黑话,欢迎补充

      黑话 解释 备注
      DAU 每日使用过bot的用户数量
      主动消息 Bot主动推送的消息 每月只能发送4条,群主/管理员可关闭
      被动消息 Bot回复用户的消息 相比主动消息没有发送数量限制 但是会随机吞消息
      私域 相对较小的使用场景 女生自用 有加群数量限制,相对限制较少,能监听的事件更多
      公域 相对较大的使用场景 公交车 只能监听at相关的事件(目前群bot只能选这个 可能是接口没做好)
      全量 相对于白名单的概念,谁都可以用 bot默认是白名单,切换时要审核

      后话

      之前对第三方bot疯狂打压,在笔者看来只是为了给官方接口铺路,但腾讯做了五年还是这样实在令人失望。

      以上,想到再补充,本文随意转载,CC-BY-NC-SA 4.0

      前情提要: https://mirai.mamoe.net/topic/2526/qq群bot官方接口-与使用体验

      发布在 开发交流
      Dituon
      Dituon
    • InkEngine —— 自定义 互动游戏 / 互动小说 引擎

      支持 Inkle / Ink: 一种用于游戏的叙述性脚本语言。
      添加了拓展函数以实现在群聊中进行互动游戏。

      Ink 是一种用于游戏的叙述性脚本语言,可以帮助开发者以轻松的语法快速书写多分枝互动游戏,同时提供了强大的拓展性。可快速编写包含 经济 好感度 签到 等系统的互动游戏。

      简单语法如下

      // 每天签到可获取随机 10 - 20 金币
      VAR money = 0
      
      ===main===
      
      新的一天开始了!
      
      你目前有 {money} 金币。
      
      + [签到]
        ~ temp m = RANDOM(10, 20)
        ~ money += m
        签到成功!你获得了 {m} 金币!
        -> main
      
      

      开发计划:

      • 多故事切换
      • 支持OneBot协议
      • 补充
        README.md

      注意:

      本插件不内置任何游戏,需要加载编译后 main.ink.json 格式。

      作为示例的互动游戏将在近期发布。

      编写教程将在近期发布。

      链接:

      Github

      SDK

      发布在 插件发布
      Dituon
      Dituon
    • [教程] 使用 cloud flare worker 实现免费接口代理

      背景

      看到了 @xiatianYa 的求助帖与一些回复 https://mirai.mamoe.net/topic/2575/ 发现很多人不知道 cloud flare worker 免费服务,作为资深白嫖怪教大家如何白嫖接口代理。

      要求

      1. 您冰雪聪明的大脑 (必须)
      2. 一点js基础 (可选)
      3. 心仪的域名 (可选)
      4. 赞美的心 (可选)

      介绍

      cloud flare worker 是 cloud flare (知名免费CDN厂商) 一段时间之前推出的一项免费服务, 允许在CDN服务器上运行js脚本或wasm

      截止到这篇文章写完的时候,这仍是一项长期免费服务,免费套餐为每天 100000 个请求, 大概是100个人 每人请求100次, 或0.01个人 每人请求10000000次

      步骤

      如果您在下方任何步骤中出现了问题,请移步下方的 常见问题 小结

      1. 注册/登录账户 https://dash.cloudflare.com/login

      2. 创建Worker
        d357a244-1af3-4616-b6f1-852de5ae6990-image.png
        cc30ef6c-bab3-40d2-9c5d-2a1643aacb00-image.png

      3. 设置一个便于记忆的 Name (可选)
        c2557bc5-3027-4944-ac42-6748f73cca7f-image.png

      4. 创建成功,点击修改代码
        cb55515f-8b1e-4c20-8eb8-ac6e37ea8dd3-image.png

      5. 写代码
        以背景里的求助帖为例,需要代理的接口为
        https://api.steampowered.com/IGameServersService/GetServerList/v1/?key=A96B85AE8E6A3E52C726563D902C2B77&filter=addr\180.102.24.61:16261
        则可以用以下代码代理

      export default {
        async fetch(request, env, ctx) {
          // 指定要代理的目标地址
          const targetURL = 'https://api.steampowered.com/IGameServersService/GetServerList/v1/?key=A96B85AE8E6A3E52C726563D902C2B77&filter=addr\\180.102.24.61:16261';
          const modifiedRequest = new Request(targetURL, request);
          const response = await fetch(modifiedRequest);
      
          return new Response(response.body, {
            status: response.status,
            statusText: response.statusText,
            headers: response.headers,
          });
        },
      };
      
      1. 保存 (建议先进行请求测试(葱绿色框框))
        2948f5e8-fe19-4357-82fc-2eca2289e421-image.png

      2. 大功告成,可以通过图上的链接访问
        https://hello-world-autumn-fire-2598.d2n.workers.dev/
        d71ef107-105c-41bf-ba7e-6843a5cc4187-image.png

      3. 可选:域名太丑怎么办,使用自己的域名代理
        1facbbdb-b841-43fc-a4a0-99a50a5c6d34-image.png bfe80147-ce11-410e-8d8a-19e5f731723d-image.png
        于是就可以在 https://d2n.moe/helloworker 访问了

      常见问题

      Q: 访问不了 cloudflare 怎么办啊
      A: 参考下一条问题

      Q: 保存不了代码怎么办啊
      A: 参考下一条问题

      Q: 默认的 workers.dev 域名访问不了怎么办啊
      A: workers.dev 的域名在中国某些地区惨遭DNS污染,可修改hosts访问,或使用梯子进行编辑,挂载到自己的域名下即可

      Q: 挂载时找不到域名怎么办啊
      A: 先在cloudflare中添加您的域名

      Q: 怎么添加域名啊
      A: https://www.bing.com/search?q=cloudflare+添加域名

      Q: 遇到的问题这里没有怎么办啊
      A: 跟帖补充

      后话

      因为便于演示的原因,只代理了某个特定的地址,通过修改代码实现代理任意地址或动态地址等,也可实现修改header实现cors转发等高级操作,本教程抛砖引玉,希望坛友发挥主观能动性

      如果你来自遥远的未来,本文的大部分信息可能已经失效(例如示例数据,代码,域名等), 请开动脑筋找到适合当前时代的解决方案, 也可以回帖询问我, 如果不回复就是已经过世了,请用赞美之心为我默哀几秒

      如果你不会写代码,可以用以下咒语询问GPT等有代码写作能力的聊天AI

      帮我写一个 cloudflare worker,风格为ESM,样式为: 
      
      export default {
        async fetch(request, env, ctx) {
          return new Response('Hello World!');
        },
      };
      
      如果你有访问网络的能力,文档地址为 https://developers.cloudflare.com/workers/
      
      代理xxx地址,提供xxx功能,oooxxx
      

      如果你的AI太笨, 写的代码没法用, 欢迎回帖求助

      如果有任何其它问题,欢迎回帖提问

      本文随意转载 CC-BY-NC-SA,请先告知我

      发布在 开发交流
      Dituon
      Dituon
    • 年度总结插件开发计划

      刚刚看到了腾讯官方的年度总结界面,感觉数据统计项不尽人意,打算开发一个统计群内聊天数据,在 年底/月底 进行总结的插件。

      本人文化造纸堪忧,想不到有趣的统计项,希望坛友集思广益提出建议。

      一些有趣的统计项:

      群员

      对每个群员分别统计

      • 被回复的次数
      • 被at的次数
      • 最常at的人
      • 最常被某人at
      • 被禁言的次数&时间
      • 消息总字数
      • 更改群名片次数
      • 上传文件总数量&大小
      • 发送图片数量
      • 撤回消息的数量
      • 戳一戳次数
      • 最常戳&被戳的人
      • 最常发送的表情包

      拓展 (可能需要依赖高级拓展或需要更多性能, 不一定会开发)

      • 使用最多的emoji
      • 年度热词 (通过分词统计周期内出现最多的词语)
      • 个人词云 (同上)

      群聊

      对群聊内所有事件进行统计

      • 最常被at的人
      • 最常at别人的人
      • 最常戳&被戳的人
      • 发送最多的表情包
      • 被禁言次数&时间最多的人
      • 发言次数&字符最多的人
      • 发图片最多的人
      • 发链接最多的人
      • 上传文件最多&最大的人
      • 被发送最多的链接域名
      • 每天聊天到最晚的人 (以 05:00 为界限)
      • 每天发送第一条消息最多的人 (同上)
      • 被下载最多&最大的文件
      • 发言天数最多的人

      拓展

      • 被使用最多的emoji
      • 年度热词&词云 (同上)
      • 被回复最多的图片&消息

      有更好的想法,欢迎补充😍

      发布在 开发交流
      Dituon
      Dituon
    • RE: InkEngine —— 自定义 互动游戏 / 互动小说 引擎

      用作示例的游戏因为太色情不得不延时发布,谢谢大家的支持

      发布在 插件发布
      Dituon
      Dituon
    • RE: 樱&花动漫接口逆向

      更新: 爬虫示例代码

      export const config = {
          fetchHeader: {
              "Sec-Ch-Ua": '"Not_A Brand";v="8", "Chromium";v="120", "Microsoft Edge";v="120"',
              "Dnt": '1',
              "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36 Edg/120.0.0.0"
          }
      }
      
      export interface VideoInfo {
          url: string
          // type: string
      }
      
      export abstract class Parser {
          public abstract parse(raw: string): Promise<VideoInfo>
      }
      
      import fetch from 'node-fetch'
      import * as cookie from 'cookie'
      
      const exp = /www\.yhmgo\.com\/vp\/(\d+)-(\d+)-(\d+)/
      const API_BASE = 'https://www.yhmgo.com/playurl'
      const PAGE_BASE = 'https://www.yhmgo.com/vp'
      
      export class YhmgoParser extends Parser {
          async parse(url: string): Promise<VideoInfo> {
              const [match, id, channel, ep] = url.match(exp)
              if (!match) {
                  throw new Error('Invalid url')
              }
              const pageUrl = `${PAGE_BASE}/${id}-${channel}-${ep}.html`
              const page = await fetch(pageUrl)
      
              let {t1, k1} = page.headers.raw()['set-cookie'].reduce((obj, str) => {
                  return {...cookie.parse(str), ...obj}
              }, {})
              let cookies = parseCookie(t1, k1)
      
              const raw = await fetch(
                  `${API_BASE}?aid=${id}&playindex=${channel}&epindex=${ep}&r=${Math.random()}`,
                  {
                      headers: {
                          ...config.fetchHeader,
                          referer: pageUrl,
                          cookie: Object.entries(cookies)
                              .map(c => cookie.serialize(c[0], c[1]))
                              .join('; ')
                      }
                  }
              )
              const {vurl} = parseResult(await raw.text())
      
      
              return {
                  url: decodeURIComponent(vurl),
                  // type: 'm3u8',
              }
          }
      }
      
      export function parseCookie(t1: number, k1: number) {
          let pos = Math.floor(t1 / 0x3e8) >> 0x5
          let offset = 0x89a4
          let k2 = '' + (((((pos * (pos % 0x100) + 0x1) + offset) * ((pos % 0x80) + 0x1)) * ((pos % 0x10) + 0x1)) + pos)
      
          let t2 = ''
          while (true) {
              t2 = '' + Math.floor(new Date().getTime())
              let tOffset = t2.slice(t2.length - 0x3)
              let kOffset = k2.slice(k2.length - 0x1)
              if (tOffset.indexOf(kOffset) >= 0x0) {
                  break
              }
          }
      
          let start = 0x0
          let timeOffset = Math.floor(new Date().getTime() / 0x3e8) >> (0x11 + start)
          let m2t = ((((timeOffset * 0x15 + 0x9a) * (((timeOffset % 0x40) + 0xd)) * ((timeOffset % 0x20) + 0x22) * ((timeOffset % 0x10) + 0x57)) * ((timeOffset % 0x8) + 0x41)) + 0x2ef)
      
          return {
              t1, k1, t2, k2, m2t
          }
      }
      
      export function parseResult(raw: string): {
          purl: string,
          vurl: string,
          inv: string
      } {
          let result = ''
          const offset = 0x619
          const rawLength = raw.length
          for (let i = 0x0; i < rawLength; i += 0x2) {
              let temp = parseInt(raw[i] + raw[i + 1], 0x10)
              temp = (
                  (temp + 0x100000 - offset) - (((rawLength / 0x2) - 0x1) - (i / 0x2))
              ) % 0x100
              result = String.fromCharCode(temp) + result
          }
          return JSON.parse(result)
      }
      

      使用

      import {YhmgoParser} from "./parser/yhmgo";
      
      const parser = new YhmgoParser()
      const video = await parser.parse('https://www.yhmgo.com/vp/11141-2-0.html')
      console.log(video)
      
      // 输出: { url: 'https://vip.lz-cdn16.com/20230317/13224_75d9217b/index.m3u8' }
      
      发布在 开发交流
      Dituon
      Dituon
    • RE: Petpet - 生成各种奇怪的图片

      更新了3.6预览版本

      https://github.com/Dituon/petpet/releases

      发布在 插件发布
      Dituon
      Dituon
    • jvm awt 图像处理入坑/退坑指南

      前言

      作为合格的 Miraier,一定曾想过在插件中加入图像合成功能,例如绘制每日发言排行榜,漂亮的签到卡片,或只是想给色图加个滤镜避免被 QQ 服务器吃掉。

      开始进行图像处理的第一步当然是选择合适的 API,选择比努力更重要

      在 JVM 平台上,有几个主流的图形处理方案可以选择:


      Skia

      谷歌家的大小姐,地球上最流行的图形API,广泛应用于 Chromium、Android、Flutter 等知名软件中。

      虽然笔者并未在 JVM 平台上直接使用过 Skia,但以下总结的优势来自于笔者在 Rust-Skia 中的经验:

      • 性能:支持 GPU 加速,并允许编写 GPU 着色器(使用类似 GLSL 的 SKSL 语法)。
      • 漂亮:相比于调教不足的 AWT,Skia 渲染的图像无需额外调教就十分精甚细腻。

      Skia 在 JVM 平台上的短板也很明显:

      • JNI:每个平台和架构都需要单独分发代码,且本地库编译后的文件大小约为 10MB。
      • 文档:网上很难找到针对特定问题的 Java 绑定解决方案,相关的第三方库也很少。
      • 编译:如果想要使用 Skia 的扩展功能(如 SVG 段落排版等),不得不经历一场痛苦的编译与配置过程,而且为每个平台交叉编译的方式与 JVM 平台的开发习惯并不匹配。

      JavaFX

      十分古老的新技术,适合构建 UI 应用,文档和第三方库非常稀缺,因此后文不再提及。


      AWT

      作为 JDK 的一部分,AWT 是 Java 中最为流行的图形 API,无需额外依赖,基于 AWT 的知名软件包括 JetBrains 全家桶 和 Eclipse。

      与 Skia 相比,AWT 的优势如下:

      • 生态:AWT 长期以来是 Java UI 和图像处理的唯一选择,因此可以找到详尽的文档资料,调教指南与第三方库(例如本文)。
      • 依赖:捆绑在几乎所有 jvm 中,无需额外依赖。又不能选装,不用白不用

      劣势也很明显:

      • 古老:AWT 不支持许多现代特性,例如彩色 emoji 字体和亚像素级抗锯齿等。
      • 性能:编写高性能的代码需要付出大量的努力,尤其是在 AWT 中。
      • 欠草:渲染漂亮图像的过程中会遇到许多迷惑的历史遗留特性,需要加大力度调教,这也是本文诞生的原因之一。

      总而言之,笔者强烈推荐大家选择 Skia。如果您不幸入坑了 AWT,希望本文能帮助你避开一些常见的坑。

      AWT 七宗罪

      锯齿

      在 AWT 诞生的年代,给文本和图片加上抗锯齿会严重降低性能,后来为了保证和之前版本的一致性,抗锯齿选项也没有默认启用。

              g2d.setRenderingHint(
                      RenderingHints.KEY_ANTIALIASING,
                      RenderingHints.VALUE_ANTIALIAS_ON);
      
              g2d.setRenderingHint(
                      RenderingHints.KEY_INTERPOLATION,
                      RenderingHints.VALUE_INTERPOLATION_BILINEAR);
      
              g2d.setRenderingHint(
                      RenderingHints.KEY_RENDERING,
                      RenderingHints.VALUE_RENDER_QUALITY);
      
              g2d.setRenderingHint(
                      RenderingHints.KEY_STROKE_CONTROL,
                      RenderingHints.VALUE_STROKE_PURE);
      
              g2d.setRenderingHint(
                      RenderingHints.KEY_TEXT_ANTIALIASING,
                      RenderingHints.VALUE_TEXT_ANTIALIAS_ON);
      
              g2d.setRenderingHint(
                      RenderingHints.KEY_ANTIALIASING,
                      RenderingHints.VALUE_ANTIALIAS_ON);
      
              g2d.setRenderingHint(
                      RenderingHints.KEY_RENDERING,
                      RenderingHints.VALUE_RENDER_QUALITY);
      

      e99f4b74-bb0a-49a9-940a-676df30bae1b-image.png

      以上选项仅适用于文本与 Shape 抗锯齿,您会发现图片缩放绘制时还是会有锯齿和奇怪的像素点,这时就要说到...


      重采样

      • drawImage

        想象一下这段代码

              g2d.drawImage(img, x, y, w, h, null);
        

        这几乎是 AWT 中最常见的绘制图像操作,虽然速度很快,但对于缩放图像来说效果是相当糟糕的。

        ee7b3aaf-b1b7-4c43-82f6-1ebebc068be3-image.png

        所幸 AWT 中还有更漂亮的方法:

      • getScaledInstance

              img.getScaledInstance(w, h, Image.SCALE_SMOOTH);
        

        5134b434-53a8-4a62-9027-6bf359f89fc7-image.png

        确实漂亮了很多,然而代价是性能损失了几乎 1000 倍甚至 900 倍,在基准测试中,上个方法耗时为 0ms,这个方法耗时 771ms,很显然也是不能接受的。

        可以看出,AWT 对图像的缩放算法相当糟糕,还好我们有更现代的第三方库:

      • Thumbnails.size

        引入 Thumbnailator

          Thumbnails.of(image).size(w, h).asBufferedImage();
        

        143fefe2-8e96-4e5c-8408-e8ebfc30078f-image.png

        得益于第三方库实现的快速重采样算法,缩放这张图片只花费了 94ms

      • 回到 drawImage

        在缩放图像分辨率与实际绘制分辨率相近的情况下,使用 Thumbnails.size 反而会造成额外的性能开销,经过古人的实践与总结,缩放倍率在 0.5 以内使用 drawImage 是最优解,超过此倍率就要引入额外的缩放流程来保证质量。

        举例:

        当原始分辨率为 200 * 200 时, 缩放到 101 * 101 或 299 * 299 应使用 drawImage; 缩放到 99 * 99 或 301 * 301 应加入额外的缩放流程。

      • 网络请求 【Miraier 限定】 【腾讯粉丝福利】

        聪明的 Miraier 应该也想到了从网络请求入手,请求更低分辨率的图像,既能减少图片下载时间又能提升缩放时的性能。

        这是 Mirai 源码中获取用户头像的代码

        public interface ContactOrBot : CoroutineScope {
          /**
           * 头像下载链接, 规格默认为 [AvatarSpec.LARGEST]
           */
          public val avatarUrl: String
              get() = avatarUrl(spec = AvatarSpec.LARGEST)
        
          /**
           * 头像下载链接.
           * @param spec 头像的规格.
           */
          @JvmName("getAvatarUrl")
          public fun avatarUrl(spec: AvatarSpec): String {
              return "http://q.qlogo.cn/g?b=qq&nk=${id}&s=${spec.size}"
          }
        }
        

        另外,域名为 gchat.qpic.cn 的图像貌似也可以请求低分辨率版本,但笔者没有在网上找到更多信息,希望大佬跟帖补充。

      上文图像与基准测试引用自 Thumbnailator/wiki


      文字排版

      研表究明,99% 的 AWT 用户在第一次绘制文本多行文本时,一定会写出这段代码:

      g2d.drawString("多行\n文本", 20, 30);
      

      虽然几乎所有的图形 API 都会有这个问题,但 AWT 的高级排版类(例如 FlowLayout)是 Swing UI 专属,无法直接在图像内绘制文字。

      在 Skia 中可以使用段落扩展来快速实现排版,AWT 又输

      例如,在 AWT 中使用 LineBreakMeasurer 和 TextLayout 计算坐标实现基本的文本溢出换行:

       public void paint(Graphics graphics) {
           Point2D pen = new Point2D(10, 20);
           Graphics2D g2d = (Graphics2D)graphics;
           FontRenderContext frc = g2d.getFontRenderContext();
      
           LineBreakMeasurer measurer = new LineBreakMeasurer(styledText, frc);
           float wrappingWidth = getSize().width - 15;
      
           while (measurer.getPosition() < fStyledText.length()) {
               TextLayout layout = measurer.nextLayout(wrappingWidth);
      
               pen.y += (layout.getAscent());
               float dx = layout.isLeftToRight() ?
                   0 : (wrappingWidth - layout.getAdvance());
      
               layout.draw(graphics, pen.x + dx, pen.y);
               pen.y += layout.getDescent() + layout.getLeading();
           }
       }
      

      (更多示例请参考 甲骨文官方文档)

      当然,上方的代码只是甲骨文的卖家秀,实际渲染环境中没机会写出这么简洁的代码,因为会碰到更头痛的问题:


      字体回退

      想象这样一个场景:

      var font = StyleContext.getDefaultStyleContext().getFont("漂亮的英文字体", Font.PLAIN, 24);
      g2d.setFont(font);
      g2d.drawString("Hello 世界!", 20, 30);
      

      上方代码在 Windows 和 Linux 平台上会渲染为 【Hello 口口口】,在 Mac 平台上会正常渲染。

      造成这种差异的原因是 JVM 逻辑字体配置 fontconfig.properties,仅在某些平台上生效,深层次的原因是 AWT 在不支持的平台上使用旧版本 HarfBuzz 进行整形,并不会自动回退为支持的字体。

      对于这种情况的解决方案:

      • 强迫用户使用 MacOS

      • 强迫用户改用 JetBrainsRuntime JVM

      • 自己实现字体回退逻辑:

        例:

              Font mainFont = StyleContext.getDefaultStyleContext().getFont("漂亮的英文字体", Font.PLAIN, 24);
              Font fallbackFont = StyleContext.getDefaultStyleContext().getFont("回退的中文字体", Font.PLAIN, 24);
              AttributedString str = new AttributedString("Hello 世界!");
        
              int textLength = text.length(); 
              result.addAttribute(TextAttribute.FONT, mainFont, 0, textLength);
        
              boolean fallback = false;
              int fallbackBegin = 0;
              for (int i = 0; i < text.length(); i++) {
                  boolean curFallback = !mainFont.canDisplay(text.charAt(i));
                  if (curFallback != fallback) {
                      fallback = curFallback;
                      if (fallback) {
                          fallbackBegin = i;
                      } else {
                          result.addAttribute(TextAttribute.FONT, fallbackFont, fallbackBegin, i);
                      }
                  }
              }
              
              g2d.drawString(str.getIterator(), 20, 30);
        

        聪明的读者一定已经发现:

        上文中 LineBreakMeasurer () 构造函数刚好需要 str.getIterator() 获取的 AttributedCharacterIterator!

      将两段代码结合一下刚好能实现最基础的文本段落渲染。

      全局字体

      看到这里,大部分爱偷懒的 Miraier 都会想到【回退字体也太麻烦了,直接用中文字体渲染就好了】偷懒方案可跳转至下文 #偷懒的最终方案

      这会导致以下问题:

      • 无法渲染字体不支持的字符,例如 阿拉伯语,印地语 等。
      • 无法同时用漂亮的英文字体和中文字体回退。
      • 无法渲染 Emoji (很多用户的 ID 包含 Emoji,不能渲染的话就太糟了)

      于是诞生了终极方案:

      1. 默认字体无法显示,启用回退方案。
      2. 调用 GraphicsEnvironment.getLocalGraphicsEnvironment().getAllFonts() 取得注册的所有字体进行遍历。
      3. 找到能显示的字体应用到字符。

      程序初始化时通过测试各语言码点分类进行性能优化:

      1. 遍历所有字体并测试各语言覆盖范围,例如 AB00-AB5F 范围为越南傣语,测试字体是否支持 AB00
      2. 渲染时快速判断字符码点范围查表找出回退字体

      需要注意的是:

      • Emoji 和部分扩展区字符会占用多个码点,超出 char 的范围,例如 Emoji 😊 为 Character.toChars(0x1F600)

      • Emoji 可能包含连字符 0x200d, 被连字符组合的 Emoji 不应该被分开渲染,例如 👩‍👩‍👧‍👧 包含 4 个子表情与 3 个连字符,占用 11 个 char

        52e51909-d65e-4dd6-8cac-a18e9d6559be-image.png

      Emoji 没有颜色😭

      868e08b0-817a-47b7-8716-89c0aee117a7-image.png

      遗憾的是, AWT 使用的 HarfBuzz 版本不支持 CPAL, SBIX 等彩色字体扩展,只能通过定制 JVM 并替换 HarfBuzz 依赖解决。

      万幸,JetBrains 在开发 IDE 时注意到了这点,目前在 AWT 平台渲染彩色 Emoji 唯一的解决方案是使用 JetBrainsRuntime JVM。

      偷懒的最终方案

      在 AWT 中,可以使用 "Dialog" 获取当前系统字体, 而且在任何平台都能正确应用 JVM 逻辑字体配置 fontconfig.properties 中定义的回退方案。

      没有调用 g2d.setFont() 时绘制的文字也会使用 "Dialog" 字体

      var font = StyleContext.getDefaultStyleContext().getFont("Dialog", Font.PLAIN, 24);
      g2d.setFont(font);
      

      作为偷懒的惩罚,使用自定义字体时不支持的字符会变为 【口口口】,详见上文。


      后话

      本来想写的 AWT 七宗罪只写了四条,请期待本贴持续更新。

      最后,以上提到的几点在 Skia 中完全无需任何调教,笔者强烈推荐大家选择 Skia。

      本贴欢迎提问 AWT 与 Rust-Skia 相关问题,别的我也不会。

      本文随意转载,转载前请告知我。

      CC-BY-NC-SA 4.0


      相关链接: Petpet

      发布在 摸鱼区
      Dituon
      Dituon
    • RE: 今日水群排行榜插件

      文字可以加一个抗锯齿 效果就更好了

      g2d.setRenderingHint(RenderingHints.KEY_TEXT_ANTIALIASING,RenderingHints.VALUE_TEXT_ANTIALIAS_ON);
      

      awt 的头像缩放算法也会产生奇怪的锯齿, 可以多次缩小, 每次缩小50%直到需要的分辨率, 或直接使用qq号获取低分辨率的头像

      发布在 插件发布
      Dituon
      Dituon
    • RE: 【MiraiUI】一个基于Mirai-http-api的WebQQ

      @Ybot 不考虑一下垂直标签页或者分组吗😨 这么多标签页是怎么分清的

      发布在 项目发布
      Dituon
      Dituon
    • RE: Mirai现在有保存聊天记录的插件吗

      @unikevin
      https://github.com/cssxsh/mirai-hibernate-plugin
      通过数据库储存聊天记录

      发布在 使用交流
      Dituon
      Dituon
    • RE: QSign 9.0.55-9.0.95(公益 API发布)

      @Alceatraz 开源意味着腾讯也能看到并进行针对性检测,目前这种形态确实是无奈之举。

      发布在 技术交流板块
      Dituon
      Dituon
    • RE: Petpet - 生成各种奇怪的图片

      @YoinSama 请提交issue,近期更新

      发布在 插件发布
      Dituon
      Dituon
    • RE: 怎么利用个人qq作机器人,转发站点内的系统通知给站点所有用户

      @rong_xiaoli 和账号有关,一般来说老帐号与充过钱的账号被风控概率更小,风控强度 私聊>群聊>频道 基本是玄学问题

      发布在 使用交流
      Dituon
      Dituon
    • 历史上的今天

      想找一些历史今天数据,发现没有那种二刺猿的...

      干脆自己写一个x


      项目地址

      目前数据


      today In History

      历史上的今天

      示例网页

      结构

      data 为根目录, 目前有 anime 和 meme 两个子类

      子类下的日期按照mm-dd格式排序, 日期文件夹内储存当日事件数据

      事件数据为 json 格式, 标准如下:

      (以 data/meme/05-24/cherry.json 为例)

      {
          "id": "cherry",
          "res": ".png",
          "date": "2016-5-24",
          "title": "陈睿 —— b站未来有可能会倒闭,但绝不会变质。"
      }
      
      • id: 用于识别此事件, 同一日期下不能重复
      • res: 资源文件(图片格式); 可简写为拓展名(".png"等价于"cherry.png")
      • date: 日期, yyyy-mm-dd 格式
      • title: 事件描述,自由发挥

      各级目录下由index.json索引

      提交

      对事件进行垃圾分类是非常麻烦的, 本项目由机器人完成自动分类

      在子类下有autoFile目录, 可直接提交更改到此目录

      机器人会在几秒钟内自动将事件移动到正确目录

      规范

      1. 对于具体年份未知的事件, 应当以作品发行年份作为事件年份
      2. 对于设定发生在未来或古代的事件, 年份范围为0001 - 9999
      3. 对于不存在的日期, 如12月32日, 应向下取最接近的日期

      如果数据够多我就写个机器人插件,每天到点发送😘

      发布在 摸鱼区
      Dituon
      Dituon
    • RE: Petpet - 生成各种奇怪的图片

      @立于羽下 @tsudzuki 已更新,使用 1.4 版本

      发布在 插件发布
      Dituon
      Dituon
    • setu-api —— 基于原生php的色图api, 封装了 Pixiv , 萌娘百科 , SauceNAO , trace.moe 等接口

      项目地址

      setu-api

      很久很久之前写的api, 近期在群友要求下开源

      封装了 Pixiv插图/小说, 萌娘百科语音, SauceNAO/trace.moe图片搜索

      基于 php 7.0+, 可轻易改写为php 5.6或更低版本

      部署

      仅需原生php 7.0+, 无需安装第三方库

      请求

      请求方式: GET

      使用 type 区分搜索模式

      type 说明 参数 返回 平台
      get_url_img 通过URL获取图片(作为反代服务器) url: 图片地址 IMAGE Pixiv
      get_pid_img 通过Pid获取图片 pid: Pid ,<br/> page: 页数(默认为1) IMAGE Pixiv
      get_pid_novel 通过Pid获取小说(不包含系列) pid: Pid ,<br/> page: 页数(默认为1) JSON Pixiv
      ranking_img 获取Pixiv日榜图片 r: 排名 IMAGE Pixiv
      random_img 随机色图 tag: 标签,<br/> r18(见下文) JSON lolicon.app
      random_voice 随机角色语音 tag: 标签 JSON 萌娘百科
      search_img 图片搜索 tag: 标签,<br/> r: 排名,<br/> r18(见下文) <br/> mode(见下文) JSON Pixiv
      search_novel 小说搜索 tag: 标签,<br/> r: 排名,<br/> r18(见下文) <br/> mode(见下文) JSON Pixiv
      img_search_img 以图搜图 url: 图片地址 JSON SauceNAO trace.moe

      r18

      可以为空, 空或0为全年龄向, 其它数值为R-18模式

      注意: 程序根据Pixiv作品Tag判断r18, 可能有误判现象

      mode

      搜索模式(enum)

      mode search_img search_novel
      default 0 按时间顺序搜索2000收藏数以上的作品 按时间顺序搜索50收藏数以上的作品
      top 1 按收藏数量顺序搜索 按收藏数量顺序搜索
      enhanced 2 按时间顺序搜索100收藏数以上的作品 按时间顺序搜索1收藏数以上的作品

      返回

      IMAGE

      content-type: image/jpeg

      可直接作为图片处理

      JSON

      content-type: application/json

      • type: 类型
      • r18: null 或 R-18 或 R-18G
      • title: 标题
      • url: 资源URL, 用于音频地址, 小说封面等
      • caption:简介, 包含收藏数, 标签等信息
      • pid:Pid (str)
      • page:当前页数
      • content:(小说专用) 正文
      type 说明 非空字段
      image 图片 r18 title url pid page
      voice 音频 url
      anime 图片搜索返回的数据 title url caption
      novel-oneshot 单篇小说 r18 title url pid page caption content
      novel-series 系列小说 r18 title url pid page caption content

      后话

      如果此程序和您预期的一样正常工作,请给我一个 star

      欢迎提交任何请求

      交流群: 534814022

      发布在 其他项目发布
      Dituon
      Dituon
    • RE: 忽然想起以前的一个插件

      @LiangChan
      你是否在找:

      https://mirai.mamoe.net/topic/1412/mutegams-一个满足群友抖m需求的禁言游戏合集-banme-轮盘赌-决斗-21点/2

      发布在 摸鱼区
      Dituon
      Dituon
    • RE: Petpet - 生成各种奇怪的图片

      @dr_chenzs 使用 sendImage 快速进行二次开发

      发布在 插件发布
      Dituon
      Dituon
    • 1
    • 2
    • 3
    • 1 / 3