Files
laoyang 3fcaf25c58 3.2.36 新增「自动头像」:按邮箱本地生成,14 套 CC0/MIT 风格可选 #5
为没有头像的访客按邮箱确定性地生成一张 SVG 头像,替代 Gravatar
那个灰底"神秘人"(cn.gravatar.com 在国内已超时,不勾加速时头像
干脆是空白)。全程纯 PHP,零外网请求、零落盘。

后台「主题设置」新增「自动头像」复选框 + 风格列表:勾上之后才
出现 14 套风格,点一下即时换 16 格预览,保存后全站生效。

实现要点(都是踩出来的):
- PRNG 必须与 DiceBear 9.x 逐位一致:xorshift + hashSeed 全程跑在
  JS 的 int32 语义里,PHP 64 位整数要逐步 i32() 归一;
  charCodeAt 按 UTF-16 码元取值,emoji 必须真做代理对拆分。
- pickComponent 有简繁两版:只有 glass/rings/shapes/thumbs 额外取
  三次整数(rotation/offsetX/offsetY)。给简版也补三次 ⇒ PRNG 多走
  3N 步 ⇒ 31 套里 27 套全错。编译期算 pc_has_ints 才治得住。
- 自带人类调色板:官方默认配色几乎全单值,30 个邮箱只出 2 种配色,
  那才是"默认头像丑"的真因,光换画风解决不了。
- 只收录 CC0/MIT(13 CC0 + 1 MIT),CC BY 那 13 套要求署名,一律不收——
  主题装给用户用,用户不会维护署名,收进来等于转嫁合规义务。

接入 get_avatar 的两个关键修正:
- 必须先看原 src 是不是 Gravatar 家族域名再决定是否替换。原先无条件
  替换会把媒体库头像、外链头像、别的插件处理过的 data: 头像一起盖掉。
- 必须删 srcset。只改 src 留着 srcset,浏览器优先用 srcset,
  生成的图根本不会被加载(现象是"代码明明换了,页面还是老样子"),
  且不能重建 <img> 标签(会丢掉评论头像 CSS 依赖的 class)。

后台三处必须这么做:
- body class 由 PHP 输出(admin_body_class),否则每次进设置页/保存后
  重载都会先显示风格列表再藏起来,闪一下。
- 事件监听 radio 的 change,不能监听 label 的 click —— label 会把
  click 转发给关联 input,冒泡回来再触发一次 ⇒ 同一风格发两次请求
  (实测 hits 2→4)。
- 首屏已渲染的 16 格写进 JS 缓存,否则点一圈回到默认风格会白请求一次。

不依赖任何可选扩展:不用 mbstring(未安装时 mb_substr 是 Fatal error,
整站白屏)、不用 gd/imagick。UTF-8 切分/截断/大写全部自己实现。
PHP 7.0 语法,PHP 8.5 下 E_ALL 无任何 warning。

验证:
- 14 套 × 6 种子 = 84 个用例与 DiceBear 官方 Node 输出逐字节一致,
  PHP 8.4 / 8.5 / 无 mbstring(php -n)三种环境均 84/0。
- 静态守卫 55/0;反向验证 17 项注入全部被捕获。
- 后台面板浏览器验证 26/0(显隐、几何、点选、缓存、连点、零报错)。
- 包内内容契约 24/0;包校验反向验证 6 项注入全部被捕获。
- 单次渲染稳态 0.047~0.105ms,峰值内存 2~4MB。
2026-10-25 18:38:11 +08:00

109 lines
5.4 KiB
PHP

<?php
return array(
'name' => 'shapes',
'meta' => array(
'title' => 'Shapes',
'creator' => 'DiceBear',
'source' => 'https://www.dicebear.com',
'license_name' => 'CC0 1.0',
'license_url' => 'https://creativecommons.org/publicdomain/zero/1.0/',
),
'view_box' => '0 0 100 100',
'clip_default' => true,
'has_pre_create' => true,
'has_post_create' => false,
'is_initials' => false,
'attr_pairs' => array(
array('viewBox', '0 0 100 100'),
array('fill', 'none'),
array('shape-rendering', 'auto'),
),
'bg_colors' => array('0a5b83', '1c799f', '69d2e7', 'f1f4dc', 'f88c49'),
'bg_type' => array('solid'),
'bg_rotation' => array('0', '360'),
'body_template' => '<g transform="matrix(1.2 0 0 1.2 -10 -10)">@@k:shape1@@</g><g transform="matrix(.8 0 0 .8 10 10)">@@k:shape2@@</g><g transform="matrix(.4 0 0 .4 30 30)">@@k:shape3@@</g>',
'pick_order' => array('shape1', 'shape2', 'shape3'),
'bool_order' => array(),
'color_order' => array(
'shape1' => 'shape1Color',
'shape2' => 'shape2Color',
'shape3' => 'shape3Color',
),
'pc_has_ints' => true,
'pc_meta' => array(
array(
'group' => 'shape1',
'values_opt' => 'shape1',
'width' => 100,
'height' => 100,
'rotation' => array('opt' => 'shape1Rotation', 'fallback' => array('0')),
'offsetX' => array('opt' => 'shape1OffsetX', 'fallback' => array('0')),
'offsetY' => array('opt' => 'shape1OffsetY', 'fallback' => array('0')),
),
array(
'group' => 'shape2',
'values_opt' => 'shape2',
'width' => 100,
'height' => 100,
'rotation' => array('opt' => 'shape2Rotation', 'fallback' => array('0')),
'offsetX' => array('opt' => 'shape2OffsetX', 'fallback' => array('0')),
'offsetY' => array('opt' => 'shape2OffsetY', 'fallback' => array('0')),
),
array(
'group' => 'shape3',
'values_opt' => 'shape3',
'width' => 100,
'height' => 100,
'rotation' => array('opt' => 'shape3Rotation', 'fallback' => array('0')),
'offsetX' => array('opt' => 'shape3OffsetX', 'fallback' => array('0')),
'offsetY' => array('opt' => 'shape3OffsetY', 'fallback' => array('0')),
),
),
'defaults' => array(
'backgroundColor' => array('0a5b83', '1c799f', '69d2e7', 'f1f4dc', 'f88c49'),
'shape1' => array('rectangleFilled', 'ellipseFilled', 'polygonFilled'),
'shape1Color' => array('0a5b83', '1c799f', '69d2e7', 'f1f4dc', 'f88c49'),
'shape1OffsetX' => array('-65', '65'),
'shape1OffsetY' => array('-45', '45'),
'shape1Rotation' => array('-160', '160'),
'shape2' => array('rectangleFilled', 'ellipseFilled', 'polygonFilled', 'line'),
'shape2Color' => array('0a5b83', '1c799f', '69d2e7', 'f1f4dc', 'f88c49'),
'shape2OffsetX' => array('-40', '40'),
'shape2OffsetY' => array('-40', '40'),
'shape2Rotation' => array('-180', '180'),
'shape3' => array('rectangle', 'rectangleFilled', 'ellipseFilled', 'ellipse', 'polygonFilled', 'polygon', 'line'),
'shape3Color' => array('0a5b83', '1c799f', '69d2e7', 'f1f4dc', 'f88c49'),
'shape3OffsetX' => array('-25', '25'),
'shape3OffsetY' => array('-25', '25'),
'shape3Rotation' => array('-180', '180'),
),
'groups' => array(
'shape1' => array(
'rectangle' => '<path fill-rule="evenodd" clip-rule="evenodd" d="M90 10H10v80h80V10ZM0 0v100h100V0H0Z" fill="@@c:shape1@@"/>',
'rectangleFilled' => '<path d="M0 0h100v100H0V0Z" fill="@@c:shape1@@"/>',
'ellipseFilled' => '<path d="M100 50A50 50 0 1 1 0 50a50 50 0 0 1 100 0Z" fill="@@c:shape1@@"/>',
'ellipse' => '<path fill-rule="evenodd" clip-rule="evenodd" d="M50 90a40 40 0 1 0 0-80 40 40 0 0 0 0 80Zm0 10A50 50 0 1 0 50 0a50 50 0 0 0 0 100Z" fill="@@c:shape1@@"/>',
'polygonFilled' => '<path d="m50 7 50 86.6H0L50 7Z" fill="@@c:shape1@@"/>',
'polygon' => '<path fill-rule="evenodd" clip-rule="evenodd" d="M50 7 0 93.6h100L50 7Zm0 20L17.3 83.6h65.4L50 27Z" fill="@@c:shape1@@"/>',
'line' => '<path fill="@@c:shape1@@" d="M45-150h10v400H45z"/>',
),
'shape2' => array(
'rectangle' => '<path fill-rule="evenodd" clip-rule="evenodd" d="M90 10H10v80h80V10ZM0 0v100h100V0H0Z" fill="@@c:shape2@@"/>',
'rectangleFilled' => '<path d="M0 0h100v100H0V0Z" fill="@@c:shape2@@"/>',
'ellipseFilled' => '<path d="M100 50A50 50 0 1 1 0 50a50 50 0 0 1 100 0Z" fill="@@c:shape2@@"/>',
'ellipse' => '<path fill-rule="evenodd" clip-rule="evenodd" d="M50 90a40 40 0 1 0 0-80 40 40 0 0 0 0 80Zm0 10A50 50 0 1 0 50 0a50 50 0 0 0 0 100Z" fill="@@c:shape2@@"/>',
'polygonFilled' => '<path d="m50 7 50 86.6H0L50 7Z" fill="@@c:shape2@@"/>',
'polygon' => '<path fill-rule="evenodd" clip-rule="evenodd" d="M50 7 0 93.6h100L50 7Zm0 20L17.3 83.6h65.4L50 27Z" fill="@@c:shape2@@"/>',
'line' => '<path fill="@@c:shape2@@" d="M45-150h10v400H45z"/>',
),
'shape3' => array(
'rectangle' => '<path fill-rule="evenodd" clip-rule="evenodd" d="M90 10H10v80h80V10ZM0 0v100h100V0H0Z" fill="@@c:shape3@@"/>',
'rectangleFilled' => '<path d="M0 0h100v100H0V0Z" fill="@@c:shape3@@"/>',
'ellipseFilled' => '<path d="M100 50A50 50 0 1 1 0 50a50 50 0 0 1 100 0Z" fill="@@c:shape3@@"/>',
'ellipse' => '<path fill-rule="evenodd" clip-rule="evenodd" d="M50 90a40 40 0 1 0 0-80 40 40 0 0 0 0 80Zm0 10A50 50 0 1 0 50 0a50 50 0 0 0 0 100Z" fill="@@c:shape3@@"/>',
'polygonFilled' => '<path d="m50 7 50 86.6H0L50 7Z" fill="@@c:shape3@@"/>',
'polygon' => '<path fill-rule="evenodd" clip-rule="evenodd" d="M50 7 0 93.6h100L50 7Zm0 20L17.3 83.6h65.4L50 27Z" fill="@@c:shape3@@"/>',
'line' => '<path fill="@@c:shape3@@" d="M45-150h10v400H45z"/>',
),
),
);