Post

PicBorder:给截图套一个 macOS 窗口边框

A Chrome extension that wraps any image in a macOS-style window frame — built with Canvas API and Manifest V3, no backend needed.

PicBorder:给截图套一个 macOS 窗口边框

在某个网站上看到别人的截图套了一圈 macOS 窗口边框,标题栏、红绿灯、圆角,一下子就比裸图好看很多。想自己也搞一个,找了一圈没找到顺手的工具,干脆自己做一个 Chrome 插件。

做了什么

点击插件图标,会在新 Tab 里打开操作界面。把图片拖进去(也支持点击选择和 Cmd+V 粘贴截图),调好参数,点 Download 就能拿到带边框的 PNG。

可调的参数有三个:

  • Style — Light Bar / Dark Bar,对应 macOS 浅色/深色窗口标题栏
  • Radius Size — 0 到 40px,控制圆角弧度
  • Border — 外描边开关

技术上的几个决定

Manifest V3。Chrome 早就要求 v3 了,Service Worker 替代了 background page。插件的后台逻辑很简单,只做一件事——监听图标点击,打开新 Tab:

1
2
3
chrome.action.onClicked.addListener(() => {
  chrome.tabs.create({ url: chrome.runtime.getURL('popup.html') });
});

为什么开新 Tab 而不是用 Popup。Popup 窗口太窄,没法做图片预览。换成完整 Tab 之后,左边放预览区,右边放控制面板,布局随便做。

Canvas 画边框。整个 macOS 窗口——标题栏、红绿灯、圆角、描边——全是用 Canvas API 画的,核心在 drawMacWindow 这个函数里。思路是先画窗口背景,再画标题栏,然后把图片 clip 进图片区域,最后描外边框:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 标题栏
ctx.beginPath();
ctx.roundRect(0, 0, imgW, TITLEBAR_H, [radius, radius, 0, 0]);
ctx.fillStyle = titlebarBg;
ctx.fill();

// 红绿灯
[{ x: 16, color: '#ff5f57' }, { x: 36, color: '#ffbd2e' }, { x: 56, color: '#28c840' }]
  .forEach(({ x, color }) => {
    ctx.beginPath();
    ctx.arc(x, dotY, 6, 0, Math.PI * 2);
    ctx.fillStyle = color;
    ctx.fill();
  });

DPR 处理。不处理的话,Retina 屏上预览会糊。Canvas 物理像素要乘以 devicePixelRatio,然后在逻辑坐标里画:

1
2
3
canvas.width  = Math.round(imgW * dpr);
canvas.height = Math.round((imgH + TITLEBAR_H) * dpr);
ctx.scale(dpr, dpr); // 之后都在 CSS px 坐标系里操作

预览时传 window.devicePixelRatio,下载时传 1,导出的是图片原始分辨率。

预览即导出(WYSIWYG)。没有做额外的离屏 Canvas,预览用的那块 Canvas 就是最终输出。点 Download 直接 toBlob()

1
2
3
4
5
6
7
previewCanvas.toBlob((blob) => {
  const a = document.createElement('a');
  a.href = URL.createObjectURL(blob);
  a.download = currentFileName + '_macos.png';
  a.click();
  URL.revokeObjectURL(a.href);
}, 'image/png');

一个刻意的设计

标题栏高度固定 40px,不随图片缩放。

macOS 窗口的标题栏视觉上是固定高度的,如果让它跟着图片一起等比缩放,图片越小标题栏就越薄,红绿灯小得不像话。所以预览时只缩放图片区域,标题栏始终 40px:

1
2
3
4
5
6
7
const TITLEBAR_H = 40; // never scales

const imgScale = Math.min(
  availW / currentImg.width,
  (availH - TITLEBAR_H) / currentImg.height,
  1 // 不放大
);

目前够用了。后面可能会加背景填充(纯色或渐变)和自定义标题文字,先放着。

This post is licensed under CC BY 4.0 by the author.