梗图生成器

在浏览器里给一张图加上顶部和底部两行文字:字号会在 12~400 px 之间二分到刚好不超过画布宽度的 90%,中文按字素断行不会拆坏 emoji,文字先描边再填色,最后导出保留透明的 PNG 或体积更小的 JPEG,全程不经过服务器。

画布尺寸1080×1080
90% 文字区972 px
字号上限97.2 px
自适应后字号97.2 px

提示:文字框里按 Enter 即可重绘;未选择图片时预览用内置示例底图,要导出请点「载入示例底图」或选择自己的图片;关闭标签页后不会留下任何副本。

使用方法

  1. 点「选择本地图片」挑一张图,PNG、JPEG、WebP、GIF、BMP 都能读,图片只在你自己的浏览器里解码。
  2. 顶部文字已预填 当你终于找到那个工具、底部预填 结果它把活干完了,改成你要的句子即可;每个框最多 80 个字素。
  3. 字号上限默认 9(本页示例值),只接受 4~20 的整数。它不是固定字号,而是天花板:画布宽 × 比例 ÷ 100,再由二分向下收缩到能排进 3 行、且最长一行不超过画布 90%。
  4. 描边宽度默认 6 px,允许 0~20 的整数,填 0 表示只填充不描边;文字色默认白色,描边色默认黑色。
  5. 导出格式选 PNG 或 JPEG;带透明背景的图必须用 PNG,选 JPEG 后透明区会变成黑色。

这个梗图生成器能解决什么问题

一是给照片配上下两行字,不装任何 App。手机拍的底图、群聊里的截图,拖进这个梗图生成器就能加字,桌面与移动浏览器同一个页面,也不用把照片传到别人服务器上。

二是把版式冻结下来批量出图。社群通知图、抽奖图、课件插图往往是一套固定版式只换文案:四个参数定好后,后面每张只改上下两行字,笔画粗细与留白比例完全一致。

三是中英混排与 emoji 不会被断坏。群里的文字常是「订单 1234 已发货 😂」这种混排,按字符数硬切的工具会把一个 ZWJ 家族 emoji 剪成两三段,导出后只剩几个残缺的小人头。本页用 Intl.Segmenter 按字素断行,把 👨‍👩‍👧‍👦 这种序列当成一个整体。

四是文字压在杂乱背景上仍看得清。照片亮一块暗一块,白字压在浅色天空上会直接消失;描边把每个字的外轮廓包一圈深色,比半透明色块更贴合字形。本页不做表情包模板搜索、GIF 动图、多图拼接与自动抠图。

渲染原理:measureText 换行、二分自适应与描边顺序

第一块基石是 CanvasRenderingContext2D.measureText()。它返回的 TextMetrics.width 是这段文字在当前 ctx.font 下的布局宽度——含字距推进量与两侧侧边距,不是墨迹包围盒。换行判断必须用它;用墨迹宽度会让相邻两行忽宽忽窄。

汉字有一个字体无关的性质:全角字的推进量等于 1 em,也就是 1 个字号。字号取 64 px 时每个汉字占 64 px,所以 1080 宽的画布留出 90%(972 px)后单行最多放 15 个汉字;96 px 只能放 10 个,120 px 只能放 8 个。拉丁字母平均推进量约 0.5 em。

换行分两级:先按空格切词,再按字素切字符。整段话先以空格切成词逐个往当前行里加,一旦 measureText(当前行 + 下一个词).width 超过 90% 上限就把当前行推入结果、另起一行。若某个词本身比一整行还长(英文长单词、连续数字,或压根没有空格的中文句子),再退到第二级:用 Intl.Segmenter 以 granularity: 'grapheme' 切成字素序列逐个塞。这保住 emoji 完整性——👨‍👩‍👧‍👦 在字素层面是 1 个单元,码点层面是 7 个,String.prototype.length 里却是 11 个 UTF-16 码元(4 个表情各 2 个,中间 3 个 ZWJ 各 1 个);按 .length 切会把它从中间剪开。

第二块基石是字号二分。搜索区间取 [12, 画布宽 × 比例 ÷ 100] 像素,每次取中点把文字包一遍,判断两个条件:行数是否不超过 3 行,最长一行宽度是否 ≤ 画布 90%。都满足就抬高下界,否则压低上界。固定迭代 20 次,区间宽 400 − 12 = 388 px,20 次后残留 388 ÷ 220 ≈ 0.00037 px,比一个屏幕像素小三千倍;再多迭代只浪费 measureText 调用。

描边顺序不能反:必须先 strokeText 再 fillText。lineWidth 以字形轮廓线为中心向两侧各扩张一半,6 px 的描边一半压在字形内部、一半露在外面。先描边后填充,填充把内侧那一半盖回去,笔画保持饱满,露在画面上的只有外面的 3 px。反过来先填充后描边,描边会压在笔画上,白字黑边比设定值细一圈。所以你填的 6 px 是 lineWidth 名义值,肉眼看到的轮廓只有一半宽。

描边宽度不能写死成固定像素。字号跟着画布走,描边也必须跟着:固定 6 px 在 1080 宽画布上占 0.556%,换到 3840 宽只剩 0.156%。本页按字号 × 0.125 给建议值(64 px 配 8 px、96 px 配 12 px),你仍可手工覆盖。

中文字体没加载完就测量,是换行错位的元凶。微软雅黑、苹方首次渲染前可能还在异步加载,此时 ctx.font 回退到默认 sans-serif,两种字体推进量表不同,measureText 量出的宽度偏大或偏小,换行点随之偏移,最坏时导出后文字溢出画布。正确做法是渲染前 await document.fonts.ready。

最后是导出格式。PNG 是 32 位带 alpha,JPEG 是 24 位无 alpha,同像素数下 JPEG 原始位图少 33.33%(1080×1080 从 4,665,600 字节降到 3,499,200 字节),代价是转成 JPEG 后所有透明像素被合成成黑色,所以带 alpha 的底图必须选 PNG。导出接口用 toBlob() 而非 toDataURL():后者同步执行,还要把二进制转成 base64,字符数比原二进制多 33.33%,一张 500000 字节的图会变成 666668 个字符,大图时足以卡住主线程。

完整示例(数值仅作演示)

打开页面时的默认条件:未选图片(预览用 1080×1080 内置底图)、顶部 10 个字素、底部 8 个字素、字号上限 9%、描边 6 px、导出 PNG。这只默认参数下的真实中间量如下:

972 px 是 1080 的 90%,97.2 px 是它的 9%,两者相除正好是顶部那 10 个字——默认控件、示例与对照表三处咬合。

画布尺寸、字号与内存对照表

下表把常见画布边长换算成三个量:90% 文字区宽度、固定 64 px 字号下单行能排的汉字个数、整块画布的 RGBA 缓冲体积。RGBA 缓冲 = 边长 × 边长 × 4 字节,是画布在内存里的真实占用,和文件体积不是一回事。

画布边长90% 文字区64 px 单行汉字RGBA 缓冲
480×480432 px6 个0.88 MiB
640×640576 px9 个1.56 MiB
720×720648 px10 个1.98 MiB
800×800720 px11 个2.44 MiB
960×960864 px13 个3.52 MiB
1024×1024921.6 px14 个4.00 MiB
1080×1080(默认)972 px15 个4.45 MiB
1200×12001080 px16 个5.49 MiB
1440×14401296 px20 个7.91 MiB
1920×19201728 px27 个14.06 MiB
2560×25602304 px36 个25.00 MiB
3840×38403456 px54 个56.25 MiB

「加载更多尺寸」会补上 4096、5120、7680、8192 四档,其 RGBA 缓冲依次是 64.00、100.00、225.00、256.00 MiB,最后两档已接近单次分配上限。点「导出 CSV」可把整张表(含隐藏的 4 档)存成带 BOM 的 CSV,Excel 打开不乱码。

边界与精度说明

常见问题

图片会上传到服务器吗?

不会。文件通过 File 对象读进来,用 createImageBitmap 在本地解码,加字与编码都用 canvas 在你的浏览器里完成。想确认就打开开发者工具的网络面板,选图并导出,全程没有新请求。

为什么我填的字号是 9%,实际字号却不是画布宽的 9%?

因为这个 9% 是上限而非固定值。工具先按 9%(1080 画布即 97.2 px)试排,排进 3 行且最长一行不超过 90% 就采用上限,排不下才二分地往下压。想让字号尽量大就把比例往上调。

描边宽度填 6,为什么看起来只有 3 像素宽?

因为 lineWidth 以字形轮廓为中心向两侧各扩张一半,而本页先 strokeText 再 fillText,填充把压在字形内部的那一半盖了回去,露在画面上的只剩外侧一半。想要 6 px 可见外描边,名义值填 12。

我输入了 emoji,会被拆成两半吗?

不会。断行用 Intl.Segmenter 的字素粒度,一个 ZWJ 家族 emoji 无论由多少码点组成,在字素层面都算 1 个整体,要么整段留在本行、要么整段挪到下一行;字数上限也按字素算,一个这样的 emoji 只占 1 个位置。

顶部的字压在很亮的背景上看不清怎么办?

把描边宽度调大一点(比如 6 调到 12),或把描边色换成与背景反差更大的颜色。描边相当于给每个字加了一圈轮廓底,比半透明色条更贴合字形。背景太杂时,建议先用「模糊图片在线」压一下背景再回来加字。

导出的图为什么比原图还大?

因为导出走的是 PNG 无损编码,而很多照片原图是经过有损压缩的 JPEG,无损重建后的 PNG 自然更大。想控体积就改选 JPEG,或先用「图片尺寸调整」把底图缩到目标边长再来加字。

为什么竖着拍的照片导出来躺倒了?

竖拍照片的像素数据往往是横的,方向记录在 EXIF 的 Orientation 字段里。本页优先用 createImageBitmap(file, { imageOrientation: 'from-image' }),让解码器在解码阶段就把像素摆正,所以竖拍照片不会躺倒。

一次能处理多张吗?能加更多行文字吗?

都不能。这个梗图生成器刻意做成一次一张、上下各一行:大图编码是 CPU 密集操作,批量跑容易卡死标签页;而梗图的经典版式本就是顶底两句话。需要多行排版请用桌面端图像处理软件。

相关工具