使用方法
- 点「载入示例」用内置的 800×600 网格测试图,或选一张本地 PNG/JPEG/WebP。原图只在本页解码,不经过网络。
- 在「旋转角度」填度数,示例 30;也可以直接点 0°/90°/180°/270°/-90° 这 5 个吸附按钮。填完按回车立即重算。
- 选扩边填充色。示例用白色 #ffffff;原图带透明通道时选「透明」才能把 Alpha 一起带出去。
- 选导出格式,示例 PNG。点「旋转生成」后画布按新尺寸重算,再点「下载图片」存盘。
- 下方的角度对照表列出每个角度的新宽、新高与面积倍率,验证 0°/90°/180° 那几行为什么不变或只换宽高。需要拿数据做二次分析就点「导出对照表 CSV」。
旋转图片在线能解决什么问题
手机拍歪的证件照要摆正、扫描件歪了 2 度要校正、电商图需要统一倾斜角做视觉节奏——这三类活儿的共同点是旋转后画布尺寸会变,而多数人一转就发现图被裁掉一角或者多出一大片空白。旋转图片在线的核心价值就是把这个尺寸重算做对:它按矩形的轴对齐包围盒公式给出新画布,而不是简单交换宽高。
第二个场景是方向纠正。手机横拍出来的照片,EXIF 里存着 Orientation=6(顺时针 90°),在文件管理器里看是正的,一旦用某些程序重新编码,Orientation 字段被丢掉,图就躺下了。旋转图片在线读的是浏览器已按 EXIF 摆正的位图,你填 0° 得到的就是正的,再导出就把方向固化进像素了。
第三个场景是批量出图的角度对照。一组 18 张图要统一转到同一个角度,做完一次就知道每张扩边后多出多少像素,可以提前估下载体积和 CDN 流量。本页不替你改尺寸比例,也不做色彩和压缩率的调整——那两件事分别交给图片尺寸调整和图片压缩工具两个专门页面;本页只做几何旋转这一件事。
旋转原理:弧度换算、画布包围盒与重采样
第一步是单位换算。canvas.getContext('2d').rotate() 收的是弧度,不是角度,公式是 θ = deg × π / 180。这是本页最常见的翻车点:直接把 30 传给 rotate,浏览器会当成 30 弧度(约 1718.9 度),图会转成一大圈肉眼看不出朝向的形态。30 度换算是 0.523599 弧度,45 度是 0.785398,90 度是 1.570796——这三个值可以拿去对照任何一份旋转代码。
第二步是画布尺寸重算。图片本身只有 W×H,旋转 θ 后它在平面上的投影是一个外接矩形,新宽新高是:
newW = ceil(|W × cos θ| + |H × sin θ|)
newH = ceil(|W × sin θ| + |H × cos θ|)
取绝对值是因为要的是包围盒而不是有向投影,θ 转负数时结果必须一样。800×600 的图转 30 度:cos 30 = 0.866025、sin 30 = 0.5,newW = 800×0.866025 + 600×0.5 = 992.82,向上取整 993;newH = 800×0.5 + 600×0.866025 = 919.62,取整 920。
第三步是浮点防护。Math.sin(Math.PI) 返回的是 1.2246467991473532e-16 而不是 0,于是 800×1.22e-16 ≈ 9.8e-14,ceil 之后凭空多出 1 像素:800×600 转 180 度会得到 801×601 这种荒谬结果。解决办法是先把两个投影值四舍五入到 4 位小数再 ceil,本页的 r4() 就是干这个的。加上防护后 180 度、270 度、360 度都能拿回精确的 800×600 与 600×800。
第四步是重采样与画布变换顺序。正确写法是 translate(w/2, h/2) → rotate(θ) → drawImage(img, -W/2, -H/2),先平移到中心、旋转、再按左上角为原点贴回。少一步 translate,图就会绕左上角甩出去。本页把 imageSmoothingQuality 设成 high,因为双线性插值在非 90 倍数角度上会明显糊;这是拿一点速度换边缘质量,90 倍数角度本来就不用插值,不受影响。
完整示例(内置 800×600 演示图,数值仅作演示)
打开本页时画布上已经有一张 800×600 的网格测试图(页面自带,不是你的文件),角度框默认 30 度。点「旋转生成」得到:
- 弧度 0.523599
- 新画布 993 × 920 px
- 面积 913,560 px,原始 480,000 px,面积比 190.3%
- 扩边空白 433,560 px,占新画布的 47.5%
- 导出 PNG 时未压缩 RGBA 约 3,654,240 字节,即 3568.6 KB
同一张图把角度换成 0 度,新画布是 800×600,扩边空白 0;换成 90 度是 600×800,扩边空白同样是 0,因为 800×sin 90 = 800、600×cos 90 = 0 在数学上正好抵消。换成 45 度最夸张:1400 × cos 45 = 989.9495,取整 990,新画布 990×990,面积 980,100 px,是原图的 204.2%,多出的 500,100 个像素按 RGBA 算约 1953.5 KB 全是空白。45 度因此成为最亏的角度。
角度、格式与扩边对照表
角度这一列的规律很整齐:0° 与 90° 面积比都是 100.0%,180° 与 360° 加上浮点防护后也是 100.0%;15° 和 75° 面积比都是 152.3%,因为 90−15 = 75 时两个投影项只是互换位置;30° 与 60° 同为 190.3%,45° 与 135° 同为 204.2%。也就是说对同一张非正方形图片,面积倍率关于 45° 对称。
| 角度 | 性质 | 800×600 结果 | 面积比 | 像素处理 |
|---|---|---|---|---|
| 0 / 360 | 恒等 | 800×600 | 100.0% | 不重采样 |
| 90 / 270 | 转置 | 600×800 | 100.0% | 不重采样,宽高互换 |
| 180 | 中心对称 | 800×600 | 100.0% | 不重采样 |
| 15 / 75 | 锐角 | 929×787 | 152.3% | 双线性插值 |
| 30 / 60 | 常用倾斜 | 993×920 | 190.3% | 双线性插值 |
| 45 | 最扩边 | 990×990 | 204.2% | 双线性插值,模糊最明显 |
| 7.5 | 微调 | 872×700 | 127.2% | 插值,接近原图 |
扩边填充色这一列也有讲究。PNG 和 WebP 支持 alpha 通道,选「透明」时扩出来的区域是真透明,可以叠到任何底图上;JPEG 没有 alpha 通道,同一份画布导成 JPEG 时透明区会被填成黑色(也有实现填白,取决于浏览器)。所以原图是透明 PNG 又要出 JPEG 时,必须把填充色显式改成白色。选深墨 #172033 则适合深色主题的展示位,边缘不会显得脏。
导出格式另一个取舍是体积。PNG 是无损的,旋转后像素没变,体积和原 PNG 基本持平;JPEG 在浏览器里 toBlob 的质量参数默认 0.92,相当于在原图有损压缩之上再叠一层有损,细节会二次流失。已经压过一遍的图想再换角度,尽量走 PNG。纯粹是容器格式的问题(跟几何朝向无关),交给下方的图片格式转换更直接。
边界与精度:浮点余数、画布上限与二次压缩
浮点余数是本页最实在的精度陷阱。IEEE 754 双精度下 sin(π) 不为 0,1.2246467991473532e-16 乘上 800 也有 9.8e-14,ceil 之后就是多一整行/一整列像素。r4() 把投影值先 round 到小数点后 4 位再取整,把误差压到 1e-4 像素以下,远小于一个物理像素。本页 12 行对照表里 180°、270°、360° 三行能显示成 800×600 而不是 801×601,靠的就是这一层。
画布上限取决于浏览器:Chrome 与 Edge 的单边上限是 16384 像素,总面积上限 268,435,456 像素;Firefox 的总面积上限约 124,994,000 像素;Safari 保守得多,单边 4096 像素、总面积 16,777,216 像素。本页设了 8000×8000 像素(即 64,000,000 像素面积)的硬阈值,低于 Chrome 上限、高于 Safari 上限——也就是说在 Safari 上转大图仍可能拿到空白画布,这是浏览器的限制而不是本页的 bug。预算是按 8000×8000 算的 64,000,000 像素面积,比 Chrome 的 268,435,456 低得多,比 Safari 的 16,777,216 高得多,正好卡在中间偏保守的位置。
插值精度方面,非 90 倍数角度必然重采样。800×600 转 30 度后画布 913,560 像素,插值是双线性的一次性混合,边缘斜线会有一到两个像素的过渡带。要判断一张图能不能承受任意角度,看它的边缘是不是硬边(文字、线稿、图标)——硬边图对插值最敏感,45 度下文字边缘会明显发虚。证件照、扫描文字这类内容如果只需要摆正,用 90 倍数角度一步到位,零损失比任何插值质量设置都管用。
角度范围是 −180 到 180,开区间两端等价:180 和 −180 是同一个朝向。step 取 1 度时这段范围有 361 个可选值,取 0.1 度有 3601 个。步长越细不代表越准,0.1 度以下的角度变化在 800 像素画布上不足一个像素的位移,纯粹是给插值制造抖动。
常见问题
转完 180 度为什么画布变成 801×601,多出一圈白边?
这是浮点余数,不是算错。Math.sin(Math.PI) 的返回值是 1.2246467991473532e-16 而非 0,800 乘上它得到 9.8e-14,向上取整就凭空多 1 像素。本页对投影值做了四位小数防护,180 度能精确回到 800×600;自己写代码时也要加这层,否则换任何尺寸都可能随机多一列。
导出的 PNG 边缘有黑边,不是设置的黑边颜色。
填充色选的是「透明」时画布 alpha 为 0,PNG 会如实保留透明。当这个 PNG 被某些不支持 alpha 的程序(比如部分 PDF 渲染器或图片查看器)打开时,透明区会显示成黑色。把填充色改成白色再导一次即可,代价是失去透明背景。
为什么 90 度不糊,30 度糊?
90 度是轴对齐变换,新坐标的取整是整数到整数的映射,每个输出像素恰好来自一个输入像素,没有插值。30 度会让像素落在非整数位置,必须由邻近像素加权合成,高频边缘就会被平滑。差距在高对比硬边上最明显,纯色或渐变内容几乎看不出区别。
手机拍的图在电脑上是躺着的,填 0 度也没用?
现代浏览器在解码 JPEG 时已经按 EXIF 的 Orientation 字段(TIFF 标签 0x0111,共 8 种取值)把像素摆正了,本页拿到的是摆正后的位图。问题多半出在原图被某个程序重新编码时把 Orientation 字段丢了,浏览器无从判断。旋转图片在线的用法是:先试 0 度,如果躺着的角度是 90 度就填 90 或 −90 把它转回来,导出后方向就固化进像素,不再依赖这个字段。
转完的文件比原图大很多,是压缩失效了吗?
不是压缩失效,是画布变大了。800×600 转 30 度后像素从 480,000 涨到 913,560,涨了 90.3%,其中 433,560 个像素是扩出来的空白(占新画布 47.5%)。这部分像素在 PNG 里压得动(大片纯色),但在 JPEG 里属于高频噪点边缘,压不动。想要小文件就把角度改到 90 的整数倍,或者先用尺寸调整把长边压到 1000 像素以内。
大图转完画布是空白的,一片白。
超出了浏览器的画布上限。Safari 单边只有 4096 像素,Chrome 是 16384 像素,超过后 canvas.width 赋值会失败或静默丢弃绘制。本页对超过 8000×8000 的结果直接给出中文提示而不是画一张空白图。解决办法是先缩小原图再转,或者改用 90 倍数角度避免扩边——90 度只换宽高不扩面积,是大图最稳的选择。
能只旋转 90 的倍数、避开插值吗?
能,这也是扫描件和证件照场景的首选。本页的 0°/90°/180°/270°/−90° 五个按钮就是为此准备的,它们的面积比都是 100.0%,完全不扩边也不插值。代价是只能做垂直水平四个朝向,2 度那种微歪救不回来——要修正 2 度歪斜就必须接受插值,这是几何上的取舍,没有第三条路。
镜像对调和旋转有什么区别?
翻转是镜像,旋转是刚体变换。镜像把像素左右或上下对调,画布尺寸永远不变、不需要插值;旋转改变朝向,任意角度都要按包围盒重算画布并重采样。两者偶尔会连用(先转 90 度再翻转),但处理路径完全不同,所以拆成了两个独立工具(见下方相关工具)。想要的是「把图摆正」用本页,要的是「把图照过来」用翻转页。