MiraiForum

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

    Dituon 发布的帖子

    • RE: 一个简单的图床上传插件(imgloc)

      前排支持

      发布在 插件发布
      Dituon
      Dituon
    • RE: Petpet - 生成各种奇怪的图片

      更新了 1.0.0-beta3

      https://github.com/Dituon/petpet

      发布在 插件发布
      Dituon
      Dituon
    • RE: 关于近期某公益签名502的事情

      @FredYakumo 推荐 Overflow + NapCatQQ 是目前最低成本的可行方案了

      发布在 使用交流
      Dituon
      Dituon
    • RE: 腾讯官方Bot接口扫盲

      报:腾讯砍掉了bot主动发消息的功能,之前是每个月4次

      发布在 开发交流
      Dituon
      Dituon
    • RE: 神奇小入机的酒馆 - 一个在QQ中模拟SillyTavern的插件(Deepseek可用)

      支持

      发布在 插件发布
      Dituon
      Dituon
    • RE: Petpet - 生成各种奇怪的图片

      @CCC3115

      将 onebot 客户端消息格式改为 array 后重试

      推荐使用 Actions 中的 最新版本

      发布在 插件发布
      Dituon
      Dituon
    • RE: eridanus-dep 轻量化的Onebot v11 python sdk

      前排支持

      发布在 其他项目发布
      Dituon
      Dituon
    • RE: 今日水群排行榜插件

      @BigCherryBalls https://mirai.mamoe.net/topic/2776/jvm-awt-图像处理入坑-退坑指南

      发布在 插件发布
      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: 怎么利用个人qq作机器人,转发站点内的系统通知给站点所有用户

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

      发布在 使用交流
      Dituon
      Dituon
    • RE: 【MiraiUI】一个基于Mirai-http-api的WebQQ

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

      发布在 项目发布
      Dituon
      Dituon
    • RE: 请教一个关于JCompositeCommand的问题

      处理复杂的指令建议监听消息并自行处理指令事件

              GlobalEventChannel.INSTANCE.subscribeAlways(GroupMessageEvent.class, e -> {
                  var str = e.getMessage().contentToString();
                  if (str.startsWith("/command")) {
                      // ...
                  }
              });
      
      发布在 开发交流
      Dituon
      Dituon
    • RE: 怎么利用个人qq作机器人,转发站点内的系统通知给站点所有用户

      稳定的方案是用QQ官方机器人,将自己的小群设为测试群进行推送,更支持楼上的发邮件方案

      发布在 使用交流
      Dituon
      Dituon
    • RE: Achernar 云部署ai绘画服务 自动切换账号 持久化运行

      前排支持

      发布在 其他项目发布
      Dituon
      Dituon
    • RE: 今日水群排行榜插件

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

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

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

      发布在 插件发布
      Dituon
      Dituon
    • RE: Petpet - 生成各种奇怪的图片

      @Chaluo 已更新新版本,改用onebot协议

      发布在 插件发布
      Dituon
      Dituon
    • RE: 试接手Gocq的复活版本Astral-Gocq

      前排支持

      发布在 其他项目发布
      Dituon
      Dituon
    • RE: [Mirai-Native] Steam 视奸机

      前排支持

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

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

      发布在 使用交流
      Dituon
      Dituon
    • RE: 人生重开模拟器

      前排支持, 好久没见到mirai生态的新插件了x

      发布在 插件发布
      Dituon
      Dituon
    • 1
    • 2
    • 3
    • 4
    • 5
    • 14
    • 15
    • 1 / 15