diff --git a/src/editor/imageLimits.ts b/src/editor/imageLimits.ts
new file mode 100644
index 0000000..7c969a2
--- /dev/null
+++ b/src/editor/imageLimits.ts
@@ -0,0 +1,61 @@
+import { config } from "fabric";
+
+/**
+ * 把过大的位图缩到 fabric 滤镜后端处理得了的尺寸。
+ *
+ * ## 为什么需要这个
+ *
+ * fabric 的 WebGL 滤镜后端建的是一张 `config.textureSize × config.textureSize`
+ * 的 GL 画布(v7 默认 4096)。滤镜跑完之后 `copyGLTo2D` 是这么把结果拷回去的:
+ *
+ * ```js
+ * ctx.drawImage(glCanvas, 0, sourceY, targetCanvas.width, targetCanvas.height, …)
+ * ```
+ *
+ * `targetCanvas` 的尺寸是**图片的自然尺寸**。图片比 4096 大的话,这个源矩形
+ * 超出 GL 画布的边界,超出的部分**画不出来**——图片右边和下边直接缺一块。
+ *
+ * 2026-09-05 实测:一张 6000×3150 的图加上「亮度」滤镜后,只剩左边
+ * 683/1078 = 0.683 的宽度,正好是 4096/6000 = 0.6827。缺的就是超出纹理的那截。
+ *
+ * ## 为什么是缩图而不是调大 textureSize
+ *
+ * GL 画布是**正方形**且按 tileSize 一次性分配。调到 8192 就是 8192²×4 = 268 MB,
+ * 调到本机 GPU 上限 16384 是 1 GB,而且再大的图照样会溢出。
+ * 这个工具导出的是 1200×630 这个量级的封面,4096 的长边已经远超需要,
+ * 缩下来在成品里看不出任何差别,还顺带省了内存和导出耗时。
+ *
+ * ## 边界
+ *
+ * `config.textureSize` 不是常量:GPU 的 `MAX_TEXTURE_SIZE` 小于它时 fabric 会
+ * 退回 Canvas2D 后端(那条路径没有这个限制)。所以这里读运行时的值,不写死 4096。
+ */
+export function imageSizeLimit(): number {
+ return config.textureSize;
+}
+
+/**
+ * 需要缩就返回一张缩好的 canvas,不需要就原样返回。
+ *
+ * 用 `HTMLCanvasElement` 而不是再造一个 `
`:fabric 的 `_originalElement`
+ * 接受任何 `CanvasImageSource`,而 canvas 是同步就绪的,不用再等一次 onload。
+ */
+export function capImageSize(
+ el: HTMLImageElement | HTMLCanvasElement,
+): HTMLImageElement | HTMLCanvasElement {
+ const w = el instanceof HTMLImageElement ? el.naturalWidth || el.width : el.width;
+ const h = el instanceof HTMLImageElement ? el.naturalHeight || el.height : el.height;
+ const limit = imageSizeLimit();
+ if (w <= limit && h <= limit) return el;
+
+ const scale = limit / Math.max(w, h);
+ const out = document.createElement("canvas");
+ out.width = Math.max(1, Math.round(w * scale));
+ out.height = Math.max(1, Math.round(h * scale));
+ const ctx = out.getContext("2d");
+ if (!ctx) return el;
+ ctx.imageSmoothingEnabled = true;
+ ctx.imageSmoothingQuality = "high";
+ ctx.drawImage(el, 0, 0, out.width, out.height);
+ return out;
+}
diff --git a/src/editor/useEditor.ts b/src/editor/useEditor.ts
index ff785bb..919a378 100644
--- a/src/editor/useEditor.ts
+++ b/src/editor/useEditor.ts
@@ -13,6 +13,7 @@ import {
import { ref, shallowRef } from "vue";
import type { CanvasSize, LayerKind, SelectionKind, ShapeKind, ToolId } from "@/types";
import { DEFAULT_SIZE } from "./canvasSize";
+import { capImageSize } from "./imageLimits";
/**
* 编辑器核心:一个模块级单例。
@@ -250,11 +251,16 @@ export function applyLocked(obj: FabricObject, locked: boolean) {
/**
* 装一张图进画布并铺满(object-fit: cover 的等效)。
* 背景性质的图沉到最底层,否则会盖住先加进来的文字。
+ *
+ * 进来先过一道 `capImageSize`:超过 fabric 纹理上限的图加了滤镜会缺右边和下边,
+ * 原因和取舍见 `imageLimits.ts`。
*/
async function addImage(src: string, opts: { toBack?: boolean } = {}): Promise {
const c = canvas.value;
if (!c) throw new Error("画布还没挂载");
const img = await FabricImage.fromURL(src, { crossOrigin: "anonymous" });
+ const capped = capImageSize(img.getElement() as HTMLImageElement);
+ if (capped !== img.getElement()) img.setElement(capped);
const scale = Math.max(size.value.width / img.width, size.value.height / img.height);
img.set({
scaleX: scale,