裁剪比例
diff --git a/packages/drawnix/src/components/image-editor/ImageEditorCore.scss b/packages/drawnix/src/components/image-editor/ImageEditorCore.scss
index 301e2db3..df25e173 100644
--- a/packages/drawnix/src/components/image-editor/ImageEditorCore.scss
+++ b/packages/drawnix/src/components/image-editor/ImageEditorCore.scss
@@ -5,6 +5,9 @@
// 统一配色
$accent-color: #ffb366;
+// 裁剪框颜色
+$crop-border-color: #4f46e5; // 深靛蓝色,用于边框线
+$crop-handle-color: #818cf8; // 浅靛蓝色,用于控制点
$accent-hover: #ffc989;
$text-primary: #fff;
$text-secondary: rgba(255, 255, 255, 0.7);
@@ -457,6 +460,48 @@ $header-bg: rgba(30, 30, 30, 0.8);
color: $text-muted;
}
+ &__auto-trim-btn {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ gap: 8px;
+ width: 100%;
+ height: 40px;
+ border: 1px solid $panel-border;
+ border-radius: 8px;
+ background: rgba(255, 255, 255, 0.03);
+ color: $text-secondary;
+ font-size: 13px;
+ font-weight: 500;
+ cursor: pointer;
+ transition: all 0.2s ease;
+
+ &:hover:not(:disabled) {
+ background: rgba($accent-color, 0.1);
+ border-color: rgba($accent-color, 0.5);
+ color: $accent-color;
+ }
+
+ &:disabled,
+ &.loading {
+ opacity: 0.5;
+ cursor: not-allowed;
+ }
+
+ &.loading {
+ svg {
+ animation: spin 1s linear infinite;
+ }
+ }
+ }
+
+ &__auto-trim-hint {
+ margin-top: 8px;
+ font-size: 11px;
+ color: $text-muted;
+ line-height: 1.4;
+ }
+
&__confirm-btn {
display: flex;
align-items: center;
@@ -666,8 +711,9 @@ $header-bg: rgba(30, 30, 30, 0.8);
&__crop-area {
position: absolute;
- border: 2px solid $accent-color;
- box-shadow: 0 0 0 9999px rgba(0, 0, 0, 0.6);
+ // 使用 box-shadow 模拟边框,这样边框画在元素外部,不影响实际裁剪区域
+ // 第一个 box-shadow 是边框(2px 青蓝色),第二个是遮罩层
+ box-shadow: 0 0 0 2px $crop-border-color, 0 0 0 9999px rgba(0, 0, 0, 0.6);
cursor: move;
z-index: 10;
}
@@ -722,7 +768,7 @@ $header-bg: rgba(30, 30, 30, 0.8);
width: 12px;
height: 12px;
background: #fff;
- border: 2px solid $accent-color;
+ border: 2px solid $crop-handle-color;
border-radius: 50%;
box-shadow: 0 0 8px rgba(0, 0, 0, 0.5);
@@ -764,3 +810,13 @@ $header-bg: rgba(30, 30, 30, 0.8);
}
}
}
+
+// 动画
+@keyframes spin {
+ from {
+ transform: rotate(0deg);
+ }
+ to {
+ transform: rotate(360deg);
+ }
+}
diff --git a/packages/drawnix/src/components/image-editor/ImageEditorCore.tsx b/packages/drawnix/src/components/image-editor/ImageEditorCore.tsx
index 801fe6b3..a143cec9 100644
--- a/packages/drawnix/src/components/image-editor/ImageEditorCore.tsx
+++ b/packages/drawnix/src/components/image-editor/ImageEditorCore.tsx
@@ -45,6 +45,130 @@ import './ImageEditorCore.scss';
// Tooltip z-index 需要高于模态框
const TOOLTIP_Z_INDEX = 10010;
+/**
+ * 检测图片白边的配置
+ */
+interface WhitespaceDetectionOptions {
+ /** 白色阈值 (0-255),像素值高于此值被认为是白色,默认 250 */
+ threshold?: number;
+ /** 容差百分比 (0-1),允许边缘有一定比例的非白色像素,默认 0.02 */
+ tolerance?: number;
+ /** 最小内容尺寸(像素),防止裁剪过度,默认 50 */
+ minContentSize?: number;
+}
+
+/**
+ * 检测图片白边并返回裁剪区域
+ * @param img 图片元素
+ * @param options 检测配置
+ * @returns 裁剪区域,如果没有检测到白边则返回 null
+ */
+function detectWhitespace(
+ img: HTMLImageElement,
+ options: WhitespaceDetectionOptions = {}
+): { x: number; y: number; width: number; height: number } | null {
+ const {
+ threshold = 250,
+ tolerance = 0.02,
+ minContentSize = 50,
+ } = options;
+
+ const width = img.naturalWidth;
+ const height = img.naturalHeight;
+
+ // 创建 canvas 读取像素数据
+ const canvas = document.createElement('canvas');
+ canvas.width = width;
+ canvas.height = height;
+ const ctx = canvas.getContext('2d');
+ if (!ctx) return null;
+
+ ctx.drawImage(img, 0, 0);
+ const imageData = ctx.getImageData(0, 0, width, height);
+ const data = imageData.data;
+
+ /**
+ * 检查单个像素是否接近白色
+ */
+ const isWhitePixel = (index: number): boolean => {
+ const r = data[index];
+ const g = data[index + 1];
+ const b = data[index + 2];
+ const a = data[index + 3];
+ // 透明像素也视为白色
+ if (a < 10) return true;
+ // RGB 都接近白色
+ return r >= threshold && g >= threshold && b >= threshold;
+ };
+
+ /**
+ * 检查一行是否大部分是白色
+ */
+ const isRowWhite = (y: number): boolean => {
+ let whiteCount = 0;
+ for (let x = 0; x < width; x++) {
+ const index = (y * width + x) * 4;
+ if (isWhitePixel(index)) whiteCount++;
+ }
+ return whiteCount / width >= 1 - tolerance;
+ };
+
+ /**
+ * 检查一列是否大部分是白色
+ */
+ const isColWhite = (x: number): boolean => {
+ let whiteCount = 0;
+ for (let y = 0; y < height; y++) {
+ const index = (y * width + x) * 4;
+ if (isWhitePixel(index)) whiteCount++;
+ }
+ return whiteCount / height >= 1 - tolerance;
+ };
+
+ // 从上往下找第一行非白色
+ let top = 0;
+ while (top < height && isRowWhite(top)) {
+ top++;
+ }
+
+ // 从下往上找第一行非白色
+ let bottom = height - 1;
+ while (bottom > top && isRowWhite(bottom)) {
+ bottom--;
+ }
+
+ // 从左往右找第一列非白色
+ let left = 0;
+ while (left < width && isColWhite(left)) {
+ left++;
+ }
+
+ // 从右往左找第一列非白色
+ let right = width - 1;
+ while (right > left && isColWhite(right)) {
+ right--;
+ }
+
+ // 计算裁剪区域
+ const cropWidth = right - left + 1;
+ const cropHeight = bottom - top + 1;
+
+ // 检查是否有效裁剪(有白边且内容区域足够大)
+ const hasWhitespace = top > 0 || left > 0 || right < width - 1 || bottom < height - 1;
+ const contentSizeValid = cropWidth >= minContentSize && cropHeight >= minContentSize;
+
+ if (!hasWhitespace || !contentSizeValid) {
+ return null;
+ }
+
+ return {
+ x: left,
+ y: top,
+ width: cropWidth,
+ height: cropHeight,
+ };
+}
+
export interface ImageEditorCoreProps {
/** 图片 URL */
imageUrl: string;
@@ -132,6 +256,9 @@ export const ImageEditorCore = forwardRef
(null);
+ // 智能去白边检测中
+ const [isDetectingWhitespace, setIsDetectingWhitespace] = useState(false);
+
// 历史记录(撤销/重做)
const [history, setHistory] = useState([]);
const [historyIndex, setHistoryIndex] = useState(-1);
@@ -617,6 +744,38 @@ export const ImageEditorCore = forwardRef {
+ const img = imageRef.current;
+ if (!img) {
+ MessagePlugin.warning('图片未加载完成');
+ return;
+ }
+
+ setIsDetectingWhitespace(true);
+
+ // 使用 requestAnimationFrame 避免阻塞 UI
+ requestAnimationFrame(() => {
+ try {
+ const detectedArea = detectWhitespace(img);
+
+ if (detectedArea) {
+ // 清除固定比例,因为智能裁剪可能不符合预设比例
+ setAspectRatio(null);
+ // 设置裁剪区域
+ setCropArea(detectedArea);
+ MessagePlugin.success('已检测到白边,裁剪框已自动调整');
+ } else {
+ MessagePlugin.info('未检测到明显的白边');
+ }
+ } catch (error) {
+ MessagePlugin.error('检测白边失败');
+ } finally {
+ setIsDetectingWhitespace(false);
+ }
+ });
+ }, []);
+
// 键盘快捷键监听
useEffect(() => {
const handleKeyDown = (e: KeyboardEvent) => {
@@ -889,6 +1048,8 @@ export const ImageEditorCore = forwardRef
) : (