Skill: selena001/changtu-xiaohongshu-cropper (version 1.0.0)

--- name: changtu-xiaohongshu-cropper description: 长图转小红书图片切割工具 - 将任意长图按小红书 3:4 比例自动切割，支持移动端协作。使用场景：用户需要将长图（如文档截图、设计稿等）切割成小红书友好的图片格式。 --- # 长图转小红书图片切割工具 ## 🎯 使用场景 当用户需要将长图切割成小红书友好的图片时使用： 1. **移动端场景**：用户在手机 App 上，需要快速切割并发布 2. **批量处理**：多张图片需要统一尺寸和比例 3. **自动化需求**：需要自动填充空白、统一格式 ## 📋 核心功能 - ✅ 按小红书 3:4 比例切割（1080×1440px） - ✅ 自动去除底部多余内容（可配置高度） - ✅ 支持自定义起始位置 - ✅ 自动填充最后一张图片的空白 - ✅ 移动端协作流程（手机发图 → AI 切割 → 返回结果） ## 🚀 快速开始 ### 方式 1：命令行工具（推荐） ```bash # 基础用法（从顶部开始切割） python cropper.py 长图.png # 指定起始位置（如从 400px 开始） python cropper.py 长图.png --start-y 400 # 完整参数 python cropper.py 长图.png \ --start-y 0 \ --logo-height 160 \ --width 1080 \ --height 1440 \ --output ./output/ ``` ### 方式 2：GUI 工具（可视化） ```bash # 启动 GUI python cropper_gui.py # 操作步骤： # 1. 点击"选择图片" # 2. 滚动查看长图 # 3. 点击选择起始位置（红线出现） # 4. 点击"开始切割" # 5. 自动打开输出目录 ``` ## 📱 移动端协作流程 **当用户在手机 App 上时：** 1. **用户操作：** - 打开长图 - 发送原图或截图给 AI - 告知起始位置（如"从顶部开始"或"0px"） 2. **AI 操作：** - 接收图片 - 使用切割工具处理 - 按 3:4 比例切割 - 逐张发送回用户 3. **输出结果：** - 小红书友好的 1080×1440px 图片 - 已去除底部多余内容 - 可直接发布 ## ⚙️ 参数说明 | 参数 | 说明 | 默认值 | |------|------|--------| | `input` | 输入图片路径（必需） | - | | `--start-y` | 起始 Y 坐标 | `0` | | `--logo-height` | 底部切除高度 | `160` | | `--width` | 目标宽度 | `1080` | | `--height` | 目标高度 | `1440` | | `--format` | 输出格式（png/jpg） | `png` | | `--output` | 输出目录 | `./output/` | ## 🛠️ 技术规格 ### 输入要求 - 格式：PNG、JPG、JPEG、WEBP - 来源：任意长图（文档截图、设计稿等） ### 输出规格 - 尺寸：1080×1440px（3:4 比例） - 格式：PNG（默认）或 JPG - 质量：95% ### 依赖安装 ```bash pip install Pillow ``` ## 📁 文件结构 ``` changtu-xiaohongshu-cropper/ ├── SKILL.md # 技能说明文档 ├── scripts/ │ ├── cropper.py # 命令行工具 │ └── cropper_gui.py # GUI 工具 └── references/ └── README.md # 完整文档 ``` ## 💡 使用技巧 ### 技巧 1：确定起始位置 - 如果从最顶部开始：`--start-y 0` - 如果跳过标题栏：`--start-y 300`（根据实际调整） - 使用 GUI 工具可视化选择（推荐） ### 技巧 2：底部内容去除 - 默认切除高度：160px - 如果去除不干净：增加数值（如 180） - 如果损失内容：减少数值（如 120） ### 技巧 3：批量处理 ```bash # PowerShell 批量处理 Get-ChildItem *.png | ForEach-Object { python cropper.py $_.Name --start-y 0 --logo-height 160 } ``` ## ⚠️ 常见问题 ### Q1: 切割位置不准确？ **A:** 使用 GUI 工具，可以可视化选择起始位置 ### Q2: 底部内容去除不干净？ **A:** 调整 `--logo-height` 参数，增加或减少数值 ### Q3: 图片比例不对？ **A:** 确认参数是 `--width 1080 --height 1440`（3:4 比例） ### Q4: 移动端如何使用？ **A:** 发送图片给 AI，告知起始位置，AI 代为切割 ## 🔄 更新日志 ### v1.0.0（2026-03-20） - ✅ 初始版本发布 - ✅ 命令行工具 - ✅ GUI 工具 - ✅ 移动端协作流程 - ✅ 小红书 3:4 比例支持 - ✅ 自动切除底部内容 ## 📞 反馈与支持 - 快速启动：`python cropper_gui.py` - 问题反馈：联系作者

path: 发布指南.md
content: # 📦 长图转小红书图片切割工具 - ClawHub 发布指南

## ✅ 技能已创建

**技能位置：** `C:\Users\lgb\.openclaw\skills\changtu-xiaohongshu-cropper/`

**文件结构：**
```
changtu-xiaohongshu-cropper/
├── SKILL.md                 # 技能说明文档
├── scripts/
│   ├── cropper.py          # 命令行工具
│   └── cropper_gui.py      # GUI 工具
└── references/
    └── README.md           # 完整文档
```

---

## 🚀 发布步骤

### 1️⃣ 登录 ClawHub

```bash
clawhub login
```

按提示输入账号密码。

### 2️⃣ 发布技能

```bash
clawhub publish "C:\Users\lgb\.openclaw\skills\changtu-xiaohongshu-cropper" \
  --slug "changtu-xiaohongshu-cropper" \
  --name "长图转小红书图片切割工具" \
  --version "1.0.0" \
  --tags "latest,image,xiaohongshu,cropper,social-media"
```

### 3️⃣ 验证发布

访问 [clawhub.com](https://clawhub.com) 搜索 "changtu-xiaohongshu-cropper" 或 "长图转小红书"。

---

## 📋 技能信息

### 基本信息
- **名称：** 长图转小红书图片切割工具
- **Slug：** changtu-xiaohongshu-cropper
- **版本：** 1.0.0
- **标签：** latest, image, xiaohongshu, cropper, social-media

### 功能描述
将任意长图按小红书 3:4 比例（1080×1440px）自动切割，支持：
- ✅ 自定义起始位置
- ✅ 自动切除底部多余内容（可配置高度）
- ✅ 移动端协作流程
- ✅ GUI 可视化操作
- ✅ 批量处理

### 使用场景
1. 小红书博主需要将长图转换成图文笔记
2. 移动端快速处理（手机发图 → AI 切割 → 返回结果）
3. 批量处理多张长图

---

## 🎯 核心特性

### 1. 小红书 3:4 比例
- 宽度：1080px
- 高度：1440px
- 完美适配小红书图文笔记

### 2. 自动切除底部
- 默认切除底部 160px（可配置）
- 支持自定义调整

### 3. 移动端协作
- 手机发图给 AI
- 告知起始位置（如"从顶部开始"）
- AI 切割后返回结果
- 随时随地完成

### 4. GUI 可视化
- 完整长图预览（可滚动）
- 鼠标点击选择起始位置
- 实时显示切割线
- 切割完成自动打开目录

---

## 📖 使用示例

### 命令行方式
```bash
# 基础用法
python cropper.py 长图.png

# 指定起始位置
python cropper.py 长图.png --start-y 400

# 完整参数
python cropper.py 长图.png \
  --start-y 0 \
  --logo-height 160 \
  --width 1080 \
  --height 1440 \
  --output ./output/
```

### GUI 方式
```bash
python cropper_gui.py
```

### 移动端协作
```
1. 手机发送长图给 AI
2. 告知起始位置（如"从顶部开始"）
3. AI 切割后逐张返回
4. 直接发布到小红书
```

---

## 🔄 更新日志

### v1.0.0 (2026-03-20)
- ✅ 初始版本发布
- ✅ 命令行工具
- ✅ GUI 工具
- ✅ 移动端协作流程
- ✅ 小红书 3:4 比例支持
- ✅ 自动切除底部内容

---

## 📞 维护信息

- **作者：** 咖啡豆 ☕
- **创建日期：** 2026-03-20
- **最后更新：** 2026-03-20
- **问题反馈：** 联系娜娜美女

---

## 🎉 发布完成后的使用

发布成功后，其他用户可以通过以下方式安装：

```bash
# 安装技能
clawhub install changtu-xiaohongshu-cropper

# 更新技能
clawhub update changtu-xiaohongshu-cropper
```

安装后，技能会出现在：
`~/.openclaw/skills/changtu-xiaohongshu-cropper/`

---

**祝发布顺利！** 🚀


path: scripts/cropper.py
content: #!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
飞书长图切割工具 v1.0
功能：将飞书长图按 4:3 比例切割成小红书友好的图片，自动去除底部 Logo

作者：咖啡豆 [COFFEE]
日期：2026-03-20
"""

import argparse
import os
import sys
from pathlib import Path
from datetime import datetime
from PIL import Image, ImageDraw

# ============= 配置参数 =============
DEFAULT_WIDTH = 1080  # 小红书推荐宽度
DEFAULT_HEIGHT = 1440  # 3:4 比例高度（竖版）
DEFAULT_QUALITY = 95  # 图片质量
LOGO_HEIGHT = 160  # 底部 Logo 区域高度（像素，飞书固定值）


def calculate_crops(image_height, start_y=0, width=DEFAULT_WIDTH, height=DEFAULT_HEIGHT):
    """
    计算所有切割区域
    
    Args:
        image_height: 原图总高度
        start_y: 起始 Y 坐标（跳过顶部不需要的部分）
        width: 目标宽度
        height: 目标高度（4:3 比例）
    
    Returns:
        list of tuples: [(y_start, y_end), ...]
    """
    crops = []
    current_y = start_y
    
    while current_y < image_height:
        y_end = min(current_y + height, image_height)
        crops.append((current_y, y_end))
        current_y = y_end
        
        # 如果已经到最后，跳出循环
        if y_end >= image_height:
            break
    
    return crops


def fill_blank_space(image, target_height, fill_color=(255, 255, 255)):
    """
    如果图片高度不足，填充空白
    
    Args:
        image: PIL Image 对象
        target_height: 目标高度
        fill_color: 填充颜色（默认白色）
    
    Returns:
        PIL Image: 填充后的图片
    """
    width, height = image.size
    
    if height >= target_height:
        return image
    
    # 创建新画布
    new_image = Image.new('RGB', (width, target_height), fill_color)
    new_image.paste(image, (0, 0))
    
    return new_image


def remove_logo_simple(image, logo_height=LOGO_HEIGHT):
    """
    简单方法：直接裁剪掉底部 Logo 区域
    
    Args:
        image: PIL Image 对象
        Logo_height: Logo 区域高度
    
    Returns:
        PIL Image: 去除 Logo 后的图片
    """
    width, height = image.size
    
    if height <= logo_height:
        print("[WARN]  警告：图片高度小于 Logo 高度，跳过 Logo 去除")
        return image
    
    # 裁剪掉底部 Logo 区域
    cropped = image.crop((0, 0, width, height - logo_height))
    
    return cropped


def remove_logo_inpaint(image, logo_height=LOGO_HEIGHT):
    """
    高级方法：使用 OpenCV Inpaint 算法填充 Logo 区域
    （需要 OpenCV 支持，如果不可用则回退到简单裁剪）
    
    TODO: 后续版本实现
    """
    print("[WARN]  Inpaint 模式暂未实现，使用简单裁剪模式")
    return remove_logo_simple(image, logo_height)


def process_image(input_path, output_dir, start_y=0, width=DEFAULT_WIDTH, 
                  height=DEFAULT_HEIGHT, remove_logo=True, logo_height=LOGO_HEIGHT,
                  output_format='png'):
    """
    处理单张图片
    
    Args:
        input_path: 输入图片路径
        output_dir: 输出目录
        start_y: 起始 Y 坐标
        width: 目标宽度
        height: 目标高度
        remove_logo: 是否去除 Logo
        Logo_height: Logo 区域高度
        output_format: 输出格式（png/jpg）
    
    Returns:
        int: 生成的图片数量
    """
    # 打开图片
    try:
        img = Image.open(input_path)
    except Exception as e:
        print(f"[ERROR] 错误：无法打开图片 - {e}")
        return 0
    
    # 转换为 RGB 模式（处理 PNG 透明通道）
    if img.mode in ('RGBA', 'LA', 'P'):
        img = img.convert('RGB')
    
    original_width, original_height = img.size
    print(f"[INFO] 原图尺寸：{original_width} x {original_height} px")
    
    # 调整宽度（如果需要）
    if original_width != width:
        ratio = width / original_width
        new_height = int(original_height * ratio)
        img = img.resize((width, new_height), Image.Resampling.LANCZOS)
        print(f"[INFO] 调整后尺寸：{width} x {new_height} px")
        # 重要：更新 original_height 为调整后的高度
        original_height = new_height
    
    # 计算切割区域（使用调整后的尺寸）
    crops = calculate_crops(original_height, start_y, width, height)
    print(f"[CROP]  将切割成 {len(crops)} 张图片")
    
    # 创建输出目录
    output_path = Path(output_dir)
    output_path.mkdir(parents=True, exist_ok=True)
    
    # 生成文件名前缀
    input_name = Path(input_path).stem
    timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
    file_prefix = f"{input_name}_{timestamp}"
    
    # 开始切割
    count = 0
    for i, (y_start, y_end) in enumerate(crops, 1):
        # 切割图片
        cropped = img.crop((0, y_start, width, y_end))
        
        # 去除 Logo（如果是最后一张且启用了 Logo 去除）
        if remove_logo and i == len(crops):
            print(f"[CLEAN] 正在去除最后一张的 Logo...")
            cropped = remove_logo_simple(cropped, logo_height)
        
        # 填充空白（如果高度不足）
        current_width, current_height = cropped.size
        if current_height < height:
            print(f"[FILL] 最后一张高度不足，填充空白...")
            cropped = fill_blank_space(cropped, height)
        
        # 保存图片
        output_file = output_path / f"{file_prefix}_{i:02d}.{output_format}"
        
        if output_format.lower() == 'jpg' or output_format.lower() == 'jpeg':
            cropped.save(output_file, 'JPEG', quality=DEFAULT_QUALITY)
        else:
            cropped.save(output_file, 'PNG')
        
        count += 1
        print(f"[OK] 已保存：{output_file.name}")
    
    print(f"\n[DONE] 完成！共生成了 {count} 张图片")
    print(f"[DIR] 输出目录：{output_path.absolute()}")
    
    return count


def main():
    parser = argparse.ArgumentParser(
        description='飞书长图切割工具 v1.0 - 按 4:3 比例切割成小红书友好的图片',
        formatter_class=argparse.RawDescriptionHelpFormatter,
        epilog='''
示例用法:
  # 基础用法（自动检测参数）
  python cropper.py input.png
  
  # 指定起始位置（跳过顶部 200px）
  python cropper.py input.png --start-y 200
  
  # 自定义尺寸和输出格式
  python cropper.py input.png --width 1440 --height 1080 --format jpg
  
  # 保留 Logo（不去除）
  python cropper.py input.png --keep-logo
  
  # 自定义 Logo 高度
  python cropper.py input.png --logo-height 150
        '''
    )
    
    parser.add_argument('input', help='输入图片路径')
    parser.add_argument('-o', '--output', default='./output', 
                        help='输出目录（默认：./output）')
    parser.add_argument('--start-y', type=int, default=0,
                        help='起始 Y 坐标，跳过顶部不需要的部分（默认：0）')
    parser.add_argument('--width', type=int, default=DEFAULT_WIDTH,
                        help=f'目标宽度（默认：{DEFAULT_WIDTH}px）')
    parser.add_argument('--height', type=int, default=DEFAULT_HEIGHT,
                        help=f'目标高度（默认：{DEFAULT_HEIGHT}px，4:3 比例）')
    parser.add_argument('--format', choices=['png', 'jpg', 'jpeg'], default='png',
                        help='输出格式（默认：png）')
    parser.add_argument('--keep-logo', action='store_true',
                        help='保留底部 Logo，不去除')
    parser.add_argument('--logo-height', type=int, default=LOGO_HEIGHT,
                        help=f'Logo 区域高度（默认：{LOGO_HEIGHT}px）')
    
    args = parser.parse_args()
    
    # 检查输入文件
    if not os.path.exists(args.input):
        print(f"[ERROR] 错误：文件不存在 - {args.input}")
        sys.exit(1)
    
    # 打印配置
    print("=" * 60)
    print("[COFFEE] 飞书长图切割工具 v1.0")
    print("=" * 60)
    print(f"[IN] 输入：{args.input}")
    print(f"[OUT] 输出：{args.output}")
    print(f"[INFO] 尺寸：{args.width} x {args.height} px")
    print(f"[TARGET] 起始 Y：{args.start_y} px")
    print(f"[CLEAN] 去除 Logo：{'否' if args.keep_logo else '是'}")
    if not args.keep_logo:
        print(f"   Logo 高度：{args.logo_height} px")
    print(f"[FILE] 格式：{args.format.upper()}")
    print("=" * 60)
    print()
    
    # 处理图片
    count = process_image(
        input_path=args.input,
        output_dir=args.output,
        start_y=args.start_y,
        width=args.width,
        height=args.height,
        remove_logo=not args.keep_logo,
        logo_height=args.logo_height,
        output_format=args.format
    )
    
    if count == 0:
        sys.exit(1)


if __name__ == '__main__':
    main()


path: scripts/cropper_gui.py
content: #!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
飞书长图切割工具 - GUI 版本 v2.3
修复：
1. 切割位置偏移问题（坐标计算修正）
2. 小红书比例改为 3:4（竖版）
3. Logo 绿线实时更新

作者：咖啡豆 ☕
日期：2026-03-20
"""

import tkinter as tk
from tkinter import ttk, filedialog, messagebox
from pathlib import Path
import threading
from PIL import Image, ImageTk, ImageDraw

# 从主脚本导入核心功能
from cropper import (
    process_image, DEFAULT_WIDTH, DEFAULT_HEIGHT, LOGO_HEIGHT
)


class ImageCropperGUI:
    """飞书长图切割工具 GUI - v2.3 修复版"""
    
    def __init__(self, root):
        self.root = root
        self.root.title("飞书长图切割工具 v2.3 - 小红书 3:4 比例")
        self.root.geometry("900x900")
        
        # 变量
        self.input_path = None
        self.output_dir = Path("./output").absolute()
        self.original_image = None
        self.display_image = None  # 缩放后的显示图片
        self.display_scale = 1.0   # 缩放比例
        self.start_y = 0           # 起始位置
        self.logo_height = LOGO_HEIGHT  # Logo 高度
        self.tk_image = None       # Tkinter 图片对象
        self.start_line = None     # 起始切割线
        self.end_line = None       # Logo 顶部线
        self.click_line = None     # 鼠标点击位置的临时线
        
        # 创建界面
        self.create_widgets()
        
    def create_widgets(self):
        """创建界面组件"""
        # 标题
        title_frame = ttk.Frame(self.root)
        title_frame.pack(fill=tk.X, padx=20, pady=10)
        
        title_label = ttk.Label(
            title_frame,
            text="飞书长图切割工具 v2.3",
            font=("Arial", 16, "bold")
        )
        title_label.pack()
        
        subtitle_label = ttk.Label(
            title_frame,
            text="鼠标点击选择切割位置 | 滚轮滑动查看 | 小红书 3:4 比例",
            font=("Arial", 10)
        )
        subtitle_label.pack()
        
        # 图片上传区域
        upload_frame = ttk.LabelFrame(self.root, text="1. 上传图片", padding=10)
        upload_frame.pack(fill=tk.X, padx=20, pady=10)
        
        btn_frame = ttk.Frame(upload_frame)
        btn_frame.pack()
        
        self.upload_btn = ttk.Button(
            btn_frame,
            text="📁 选择图片",
            command=self.select_image,
            width=20
        )
        self.upload_btn.pack(side=tk.LEFT, padx=5)
        
        self.file_label = ttk.Label(btn_frame, text="未选择文件", foreground="gray")
        self.file_label.pack(side=tk.LEFT, padx=10)
        
        # 图片预览区域（带滚动条和鼠标交互）
        preview_frame = ttk.LabelFrame(self.root, text="2. 预览长图（点击选择位置 | 滚轮滑动）", padding=10)
        preview_frame.pack(fill=tk.BOTH, expand=True, padx=20, pady=10)
        
        # 创建 Canvas 和滚动条
        self.canvas = tk.Canvas(preview_frame, bg="#f0f0f0", highlightthickness=0, cursor="crosshair")
        self.scrollbar_y = ttk.Scrollbar(preview_frame, orient="vertical", command=self.canvas.yview)
        self.scrollbar_x = ttk.Scrollbar(preview_frame, orient="horizontal", command=self.canvas.xview)
        
        self.canvas.configure(yscrollcommand=self.scrollbar_y.set, xscrollcommand=self.scrollbar_x.set)
        
        self.scrollbar_y.pack(side="right", fill="y")
        self.scrollbar_x.pack(side="bottom", fill="x")
        self.canvas.pack(side="left", fill="both", expand=True)
        
        # 绑定鼠标事件
        self.canvas.bind("<Button-1>", self.on_canvas_click)  # 左键点击
        self.canvas.bind("<MouseWheel>", self.on_mouse_wheel)  # 滚轮（Windows）
        self.canvas.bind("<Button-4>", self.on_mouse_wheel_up)  # 滚轮向上（Linux）
        self.canvas.bind("<Button-5>", self.on_mouse_wheel_down)  # 滚轮向下（Linux）
        
        # 图片显示在 Canvas 中
        self.image_on_canvas = None
        
        # 起始位置选择区域
        position_frame = ttk.LabelFrame(self.root, text="3. 切割位置设置（点击图片或拖动滑块）", padding=10)
        position_frame.pack(fill=tk.X, padx=20, pady=10)
        
        # 起始位置
        pos_top = ttk.Frame(position_frame)
        pos_top.pack(fill=tk.X)
        
        ttk.Label(pos_top, text="起始位置 Y:").pack(side=tk.LEFT, padx=5)
        
        self.start_y_var = tk.IntVar(value=0)
        self.start_y_scale = ttk.Scale(
            pos_top,
            from_=0,
            to=1000,
            variable=self.start_y_var,
            orient=tk.HORIZONTAL,
            length=400,
            command=self.on_start_y_changed
        )
        self.start_y_scale.pack(side=tk.LEFT, padx=10)
        
        self.start_y_entry = ttk.Entry(position_frame, textvariable=self.start_y_var, width=10)
        self.start_y_entry.pack(side=tk.LEFT, padx=10)
        ttk.Label(position_frame, text="px").pack(side=tk.LEFT)
        
        # Logo 设置
        logo_top = ttk.Frame(position_frame)
        logo_top.pack(fill=tk.X, pady=5)
        
        self.logo_var = tk.BooleanVar(value=True)
        self.logo_check = ttk.Checkbutton(
            logo_top,
            text="去除底部 Logo",
            variable=self.logo_var,
            command=self.on_logo_setting_changed
        )
        self.logo_check.pack(side=tk.LEFT, padx=5)
        
        ttk.Label(logo_top, text="Logo 高度:").pack(side=tk.LEFT, padx=10)
        self.logo_height_var = tk.IntVar(value=160)  # 飞书固定值 160px
        self.logo_height_entry = ttk.Entry(logo_top, textvariable=self.logo_height_var, width=10)
        self.logo_height_entry.pack(side=tk.LEFT, padx=5)
        ttk.Label(logo_top, text="px").pack(side=tk.LEFT)
        
        # 绑定回车键事件（按回车实时更新绿线）
        self.logo_height_entry.bind("<Return>", self.on_logo_height_enter)
        
        # 图片信息
        self.info_label = ttk.Label(position_frame, text="", foreground="blue")
        self.info_label.pack(side=tk.RIGHT, padx=10)
        
        # 提示标签
        tip_frame = ttk.Frame(position_frame)
        tip_frame.pack(fill=tk.X, pady=5)
        
        self.tip_label = ttk.Label(
            tip_frame,
            text="💡 提示：在上方图片中点击任意位置，自动设置为切割起始点",
            foreground="green",
            font=("Arial", 9, "italic")
        )
        self.tip_label.pack(side=tk.LEFT)
        
        self.legend_label = ttk.Label(
            tip_frame,
            text="🔴 红线 = 起始位置  🟢 绿线 = Logo 顶部",
            foreground="purple",
            font=("Arial", 9, "bold")
        )
        self.legend_label.pack(side=tk.RIGHT)
        
        # 参数设置区域
        params_frame = ttk.LabelFrame(self.root, text="4. 切割参数（小红书 3:4 比例）", padding=10)
        params_frame.pack(fill=tk.X, padx=20, pady=10)
        
        # 第一行：目标尺寸
        ttk.Label(params_frame, text="目标宽度:").grid(row=0, column=0, sticky=tk.W, padx=5)
        self.width_var = tk.IntVar(value=1080)  # 小红书推荐宽度
        self.width_entry = ttk.Entry(params_frame, textvariable=self.width_var, width=10)
        self.width_entry.grid(row=0, column=1, sticky=tk.W, padx=5)
        ttk.Label(params_frame, text="px").grid(row=0, column=2, sticky=tk.W)
        
        ttk.Label(params_frame, text="目标高度:").grid(row=0, column=3, sticky=tk.W, padx=5)
        self.height_var = tk.IntVar(value=1440)  # 小红书推荐高度（3:4）
        self.height_entry = ttk.Entry(params_frame, textvariable=self.height_var, width=10)
        self.height_entry.grid(row=0, column=4, sticky=tk.W, padx=5)
        ttk.Label(params_frame, text="px (3:4)").grid(row=0, column=5, sticky=tk.W)
        
        # 第二行：输出格式
        ttk.Label(params_frame, text="输出格式:").grid(row=1, column=0, sticky=tk.W, padx=5)
        self.format_var = tk.StringVar(value="png")
        format_combo = ttk.Combobox(params_frame, textvariable=self.format_var, values=["png", "jpg"], width=8)
        format_combo.grid(row=1, column=1, sticky=tk.W, padx=5)
        
        # 输出目录
        output_frame = ttk.Frame(params_frame)
        output_frame.grid(row=2, column=0, columnspan=6, sticky=tk.W, padx=5, pady=5)
        
        ttk.Label(output_frame, text="输出目录:").pack(side=tk.LEFT)
        self.output_label = ttk.Label(output_frame, text=str(self.output_dir), foreground="gray")
        self.output_label.pack(side=tk.LEFT, padx=5)
        
        ttk.Button(output_frame, text="更改", command=self.select_output).pack(side=tk.LEFT)
        
        # 状态栏
        status_frame = ttk.Frame(self.root)
        status_frame.pack(fill=tk.X, padx=20, pady=5)
        
        self.status_label = ttk.Label(status_frame, text="就绪", foreground="green", font=("Arial", 10, "bold"))
        self.status_label.pack(side=tk.LEFT)
        
        self.progress = ttk.Progressbar(status_frame, mode='indeterminate', length=200)
        self.progress.pack(side=tk.RIGHT)
        
        # 操作按钮区域（突出显示）
        button_frame = ttk.Frame(self.root)
        button_frame.pack(fill=tk.X, padx=20, pady=15)
        
        # 用醒目的颜色突出开始按钮
        self.start_btn = tk.Button(
            button_frame,
            text="🚀 开始切割",
            command=self.start_processing,
            width=20,
            bg="#4CAF50",  # 绿色背景
            fg="white",    # 白色文字
            font=("Arial", 12, "bold"),
            relief=tk.RAISED,
            cursor="hand2"
        )
        self.start_btn.pack(side=tk.LEFT, padx=5)
        self.start_btn.config(state=tk.DISABLED)
        
        # 重置按钮
        ttk.Button(
            button_frame,
            text="🔄 重置",
            command=self.reset,
            width=10
        ).pack(side=tk.LEFT, padx=5)
        
        # 打开目录按钮
        ttk.Button(
            button_frame,
            text="📂 打开输出目录",
            command=self.open_output_dir,
            width=15
        ).pack(side=tk.LEFT, padx=5)
        
        # 绑定 Canvas 大小变化
        self.canvas.bind("<Configure>", self.on_canvas_resize)
        
    def on_canvas_resize(self, event):
        """Canvas 大小变化时更新"""
        if self.display_image:
            self.update_preview()
    
    def on_canvas_click(self, event):
        """鼠标点击 Canvas 选择起始位置"""
        if not self.original_image:
            return
        
        # 获取点击位置的 Y 坐标（考虑滚动）
        scroll_offset = self.canvas.yview()[0] * (self.original_image.height * self.display_scale)
        y_display = event.y + scroll_offset
        
        # 转换为原图的 Y 坐标
        y_original = int(y_display / self.display_scale)
        
        # 限制在有效范围内
        y_original = max(0, min(y_original, self.original_image.height))
        
        # 更新起始位置
        self.start_y = y_original
        self.start_y_var.set(y_original)
        
        # 更新信息
        self.update_info_label()
        
        # 绘制切割线
        self.draw_crop_lines(y_display)
        
        self.status_label.config(
            text=f"✅ 已选择起始位置：{y_original} px | 点击'🚀 开始切割'按钮继续",
            foreground="blue"
        )
    
    def on_mouse_wheel(self, event):
        """鼠标滚轮滚动（Windows）"""
        if event.delta > 0:
            self.canvas.yview_scroll(-1, "units")
        else:
            self.canvas.yview_scroll(1, "units")
    
    def on_mouse_wheel_up(self, event):
        """鼠标滚轮向上（Linux）"""
        self.canvas.yview_scroll(-1, "units")
    
    def on_mouse_wheel_down(self, event):
        """鼠标滚轮向下（Linux）"""
        self.canvas.yview_scroll(1, "units")
    
    def on_start_y_changed(self, value):
        """起始位置变化时更新"""
        self.start_y = int(float(value))
        self.start_y_var.set(self.start_y)
        self.update_info_label()
        
        # 更新切割线位置
        if self.display_image:
            y_display = int(self.start_y * self.display_scale)
            self.draw_crop_lines(y_display)
    
    def on_logo_setting_changed(self):
        """Logo 设置变化时更新"""
        self.update_info_label()
        # 重新绘制切割线
        if self.display_image and self.start_y > 0:
            y_display = int(self.start_y * self.display_scale)
            self.draw_crop_lines(y_display)
    
    def on_logo_height_enter(self, event=None):
        """Logo 高度输入框回车键处理"""
        self.update_info_label()
        # 重新绘制切割线
        if self.display_image and self.start_y > 0:
            y_display = int(self.start_y * self.display_scale)
            self.draw_crop_lines(y_display)
        self.status_label.config(
            text=f"✅ Logo 高度已更新：{self.logo_height_var.get()} px | 绿线已更新",
            foreground="blue"
        )
    
    def update_info_label(self):
        """更新信息标签"""
        if not self.original_image:
            return
        
        logo_height = self.logo_height_var.get() if self.logo_var.get() else 0
        logo_top_y = self.original_image.height - logo_height
        
        self.info_label.config(
            text=f"原图：{self.original_image.width} x {self.original_image.height} px | "
                 f"起始：{self.start_y} px | Logo 顶部：{logo_top_y} px"
        )
    
    def draw_crop_lines(self, y_display_start):
        """绘制双切割线"""
        # 删除旧线
        if self.start_line:
            self.canvas.delete(self.start_line)
        if self.end_line:
            self.canvas.delete(self.end_line)
        if self.click_line:
            self.canvas.delete(self.click_line)
        
        # 获取 Canvas 宽度
        canvas_width = self.canvas.winfo_width()
        
        # 绘制起始位置线（红色虚线）
        self.start_line = self.canvas.create_line(
            0, y_display_start, canvas_width, y_display_start,
            fill="red", width=3, dash=(10, 5)
        )
        
        # 绘制 Logo 顶部线（绿色虚线，如果启用了 Logo 去除）
        if self.logo_var.get():
            logo_height = self.logo_height_var.get()
            logo_top_y_original = self.original_image.height - logo_height
            logo_top_y_display = int(logo_top_y_original * self.display_scale)
            
            self.end_line = self.canvas.create_line(
                0, logo_top_y_display, canvas_width, logo_top_y_display,
                fill="green", width=3, dash=(10, 5)
            )
    
    def select_image(self):
        """选择输入图片"""
        file_path = filedialog.askopenfilename(
            title="选择飞书长图",
            filetypes=[
                ("图片文件", "*.png *.jpg *.jpeg *.webp"),
                ("所有文件", "*.*")
            ]
        )
        
        if file_path:
            self.input_path = file_path
            self.file_label.config(text=Path(file_path).name, foreground="black")
            self.load_preview(file_path)
            self.start_btn.config(state=tk.NORMAL)
            self.status_label.config(
                text="✅ 图片已加载 | 请点击图片选择起始位置，或直接点击'🚀 开始切割'",
                foreground="blue"
            )
            
    def load_preview(self, file_path):
        """加载完整图片预览"""
        try:
            self.original_image = Image.open(file_path)
            orig_width, orig_height = self.original_image.width, self.original_image.height
            
            # 计算缩放比例（适应 Canvas 宽度）
            canvas_width = self.canvas.winfo_width()
            if canvas_width < 10:
                canvas_width = 800  # 默认宽度
            
            self.display_scale = canvas_width / orig_width
            display_height = int(orig_height * self.display_scale)
            
            # 缩放图片用于显示
            self.display_image = self.original_image.resize(
                (canvas_width, display_height),
                Image.Resampling.LANCZOS
            )
            
            # 转换为 Tkinter 格式
            self.tk_image = ImageTk.PhotoImage(self.display_image)
            
            # 在 Canvas 中显示图片
            self.image_on_canvas = self.canvas.create_image(0, 0, anchor="nw", image=self.tk_image)
            
            # 配置 Canvas 滚动区域
            self.canvas.configure(scrollregion=(0, 0, canvas_width, display_height))
            
            # 更新起始位置滑块
            self.start_y_scale.config(to=orig_height)
            self.start_y_var.set(0)
            self.start_y = 0
            
            # 更新信息
            self.update_info_label()
            
            # 更新状态
            self.status_label.config(
                text=f"✅ 已加载：{orig_width} x {orig_height} px | 请点击图片选择起始位置",
                foreground="blue"
            )
            
        except Exception as e:
            messagebox.showerror("错误", f"无法加载图片：{e}")
            
    def update_preview(self):
        """更新预览（当 Canvas 大小变化时）"""
        if not self.original_image:
            return
        
        canvas_width = self.canvas.winfo_width()
        if canvas_width < 10:
            return
        
        # 重新计算缩放
        self.display_scale = canvas_width / self.original_image.width
        display_height = int(self.original_image.height * self.display_scale)
        
        # 重新缩放
        self.display_image = self.original_image.resize(
            (canvas_width, display_height),
            Image.Resampling.LANCZOS
        )
        
        self.tk_image = ImageTk.PhotoImage(self.display_image)
        
        # 更新 Canvas 中的图片
        self.canvas.itemconfig(self.image_on_canvas, image=self.tk_image)
        self.canvas.configure(scrollregion=(0, 0, canvas_width, display_height))
        
        # 重新绘制切割线
        if self.start_y > 0:
            y_display = int(self.start_y * self.display_scale)
            self.draw_crop_lines(y_display)
        
    def select_output(self):
        """选择输出目录"""
        directory = filedialog.askdirectory(title="选择输出目录")
        if directory:
            self.output_dir = Path(directory)
            self.output_label.config(text=str(self.output_dir))
            
    def start_processing(self):
        """开始处理图片"""
        if not self.input_path:
            messagebox.showwarning("警告", "请先选择图片")
            return
        
        # 禁用按钮
        self.start_btn.config(state=tk.DISABLED)
        self.progress.start()
        self.status_label.config(text="🔄 正在处理...", foreground="orange")
        
        # 在新线程中处理
        thread = threading.Thread(target=self.process, daemon=True)
        thread.start()
        
    def process(self):
        """处理图片（在后台线程中运行）"""
        try:
            # 获取参数
            start_y = self.start_y_var.get()
            width = self.width_var.get()
            height = self.height_var.get()
            remove_logo = self.logo_var.get()
            logo_height = self.logo_height_var.get()
            output_format = self.format_var.get()
            
            # 处理图片
            count = process_image(
                input_path=self.input_path,
                output_dir=str(self.output_dir),
                start_y=start_y,
                width=width,
                height=height,
                remove_logo=remove_logo,
                logo_height=logo_height,
                output_format=output_format
            )
            
            # 更新 UI
            self.root.after(0, self.process_complete, count)
            
        except Exception as e:
            self.root.after(0, self.process_error, str(e))
            
    def process_complete(self, count):
        """处理完成回调"""
        self.progress.stop()
        self.start_btn.config(state=tk.NORMAL)
        self.status_label.config(text=f"✅ 完成！生成了 {count} 张图片", foreground="green")
        
        # 自动打开输出目录
        if self.output_dir.exists():
            import os
            os.startfile(self.output_dir)
        
        messagebox.showinfo(
            "完成",
            f"🎉 切割完成！\n\n共生成了 {count} 张图片\n\n输出目录：{self.output_dir}\n\n已自动打开目录，可以直接使用图片了！"
        )
        
    def process_error(self, error_msg):
        """处理错误回调"""
        self.progress.stop()
        self.start_btn.config(state=tk.NORMAL)
        self.status_label.config(text="❌ 处理失败", foreground="red")
        
        messagebox.showerror("错误", f"处理失败：{error_msg}")
        
    def reset(self):
        """重置"""
        self.input_path = None
        self.file_label.config(text="未选择文件", foreground="gray")
        self.canvas.delete("all")
        self.original_image = None
        self.display_image = None
        self.tk_image = None
        self.image_on_canvas = None
        self.start_line = None
        self.end_line = None
        self.click_line = None
        self.start_btn.config(state=tk.DISABLED)
        self.status_label.config(text="就绪", foreground="green")
        self.start_y_var.set(0)
        self.width_var.set(1080)  # 小红书推荐
        self.height_var.set(1440)  # 3:4 比例
        self.logo_var.set(True)
        self.logo_height_var.set(160)  # 飞书固定值
        self.format_var.set("png")
        self.info_label.config(text="")
        
    def open_output_dir(self):
        """打开输出目录"""
        if self.output_dir.exists():
            import os
            os.startfile(self.output_dir)
        else:
            messagebox.showwarning("警告", "输出目录不存在")


def main():
    root = tk.Tk()
    app = ImageCropperGUI(root)
    root.mainloop()


if __name__ == '__main__':
    main()


path: references/README.md
content: # ☕ 飞书长图切割工具 v1.0

> 将飞书长图按 4:3 比例切割成小红书友好的图片，自动去除底部 Logo

**作者：** 咖啡豆 ☕  
**日期：** 2026-03-20

---

## 🚀 快速开始

### 1️⃣ 安装依赖

```bash
pip install Pillow
```

**可选（如果需要 OpenCV 高级功能）：**
```bash
pip install opencv-python numpy
```

### 2️⃣ 使用方法

#### 方式 A：命令行（推荐）

**基础用法：**
```bash
python cropper.py 飞书长图.png
```

**指定起始位置（跳过顶部 200px）：**
```bash
python cropper.py 飞书长图.png --start-y 200
```

**自定义参数：**
```bash
python cropper.py 飞书长图.png \
  --width 1440 \
  --height 1080 \
  --format jpg \
  --logo-height 150
```

**保留 Logo（不去除）：**
```bash
python cropper.py 飞书长图.png --keep-logo
```

#### 方式 B：GUI 界面

```bash
python cropper_gui.py
```

然后：
1. 点击"📁 选择图片"
2. 调整参数（可选）
3. 点击"🚀 开始切割"
4. 完成！

---

## 📐 参数说明

| 参数 | 说明 | 默认值 |
|------|------|--------|
| `input` | 输入图片路径（必需） | - |
| `-o, --output` | 输出目录 | `./output` |
| `--start-y` | 起始 Y 坐标（跳过顶部） | `0` |
| `--width` | 目标宽度 | `1440px` |
| `--height` | 目标高度 | `1080px` |
| `--format` | 输出格式（png/jpg） | `png` |
| `--keep-logo` | 保留 Logo | `False` |
| `--logo-height` | Logo 区域高度 | `120px` |

---

## 🎯 小红书图片规范

### 推荐尺寸
- **宽度：** 1440px（或 1080px）
- **高度：** 1080px（4:3 比例）
- **格式：** PNG 或 JPG
- **大小：** < 10MB

### 常见比例
| 比例 | 尺寸 | 用途 |
|------|------|------|
| **4:3** | 1440x1080 | 图文笔记（推荐） |
| **3:4** | 1080x1440 | 竖版封面 |
| **1:1** | 1080x1080 | 正方形图片 |
| **9:16** | 1080x1920 | 全屏封面 |

---

## 💡 使用技巧

### 1. 确定起始位置

飞书长图顶部可能有标题栏，建议先预览确定起始位置：

```bash
# 先用图片查看器打开，查看顶部需要跳过多少像素
# 然后用 --start-y 参数跳过
python cropper.py 飞书长图.png --start-y 200
```

### 2. 调整 Logo 高度

如果底部 Logo 去除不干净，调整 `--logo-height`：

```bash
# 增加 Logo 高度（去除更多）
python cropper.py 飞书长图.png --logo-height 150

# 减少 Logo 高度（保留更多内容）
python cropper.py 飞书长图.png --logo-height 80
```

### 3. 批量处理

用脚本批量处理多张图片：

```bash
# Windows PowerShell
Get-ChildItem *.png | ForEach-Object { python cropper.py $_.Name }

# Linux/Mac
for file in *.png; do python cropper.py "$file"; done
```

---

## 📁 输出示例

输入：`飞书文档_20260320.png`（高度 5000px）

输出目录：`./output/`

生成的文件：
```
飞书文档_20260320_20260320_113045_01.png  (1440x1080)
飞书文档_20260320_20260320_113045_02.png  (1440x1080)
飞书文档_20260320_20260320_113045_03.png  (1440x1080)
飞书文档_20260320_20260320_113045_04.png  (1440x1080)
飞书文档_20260320_20260320_113045_05.png  (1440x1080, 自动填充空白)
```

---

## ⚠️ 常见问题

### Q1: Logo 去除不干净
**A:** 调整 `--logo-height` 参数，增加或减少 Logo 区域高度

### Q2: 文字被切断了
**A:** 调整 `--start-y` 参数，或者手动检查切割位置

### Q3: 最后一张图太短
**A:** 工具会自动填充空白，不用担心

### Q4: 图片太模糊
**A:** 检查原图质量，或者调整 `--width` 和 `--height` 参数

### Q5: GUI 界面打不开
**A:** 确保安装了 tkinter（Python 自带，但有些系统需要单独安装）

```bash
# Ubuntu/Debian
sudo apt-get install python3-tk

# CentOS/RHEL
sudo yum install python3-tkinter

# macOS (通常已安装)
brew install python-tk
```

---

## 🔄 更新日志

### v1.0 (2026-03-20)
- ✅ 基础切割功能
- ✅ 自动去除 Logo
- ✅ 空白填充
- ✅ 命令行版本
- ✅ GUI 版本

---

## 📝 TODO

- [ ] 智能检测 Logo 位置（不用手动设置高度）
- [ ] 智能检测文字边界（避免切断）
- [ ] OpenCV Inpaint 高级去水印
- [ ] 预览切割效果
- [ ] 批量处理多张图片
- [ ] 飞书机器人集成

---

## 📄 许可证

MIT License

---

## 🙏 反馈

有问题或建议？联系咖啡豆 ☕


Security scan verdicts — ClawScan: clean | static analysis: clean | VirusTotal: clean | SkillSpector: suspicious (MEDIUM)