@clawhub-yunkai-6c00a62975
智能长截图切片工具。将超长图片(如聊天记录、长截图)按 9:16 比例智能切片,确保文字/拼音不被分割,输出 PDF/ZIP/切片图片。使用场景:用户发送长截图要求切片、分割、转 PDF 等。
---
name: long-image-slicer
version: 1.0.0
description: 智能长截图切片工具。将超长图片(如聊天记录、长截图)按 9:16 比例智能切片,确保文字/拼音不被分割,输出 PDF/ZIP/切片图片。使用场景:用户发送长截图要求切片、分割、转 PDF 等。
author: 果光
license: MIT
tags: [image, pdf, screenshot, slice, long-image]
---
# Long Image Slicer - 长截图智能切片
## 触发条件
当用户提出以下需求时触发此技能:
- 长截图切片/分割
- 聊天记录转 PDF
- 超长图片处理
- 确保文字不被截断的切片
## 工作流程
### 1. 获取源图片
**方式 A:用户直接发送**
- 保存到临时目录
**方式 B:用户提供 URL**
```bash
curl -L "<图片 URL>" -o /tmp/source.jpg
```
**方式 C:用户指定本地路径**
- 直接使用用户提供的路径
### 2. 执行切片
```bash
cd ~/.openclaw/skills/long-image-slicer
python3 scripts/slice_processor.py <源图片路径> [输出目录]
```
脚本自动:
1. 分析图片内容密度(检测文字行)
2. 智能计算切分位置(确保文字/拼音完整)
3. 生成切片图片(slice_01.jpg ~ slice_48.jpg)
4. 保存到 `输出目录/slices_v7/`
### 3. 生成 PDF
```bash
python3 scripts/create_pdf.py <切片目录> [输出 PDF 路径]
```
PDF 规格:
- A4 大小 (21.0cm × 29.7cm)
- 按高度缩放,确保所有切片完整显示
- 左右边距自动计算(相等)
- 上下边距 1cm
- 每页一张切片,垂直居中
- 页码:右下角(距右边缘 1cm,距下边缘 0.5cm)
### 4. 交付结果
输出文件:
- `输出目录/slices_v7/` - 切片图片目录
- `输出目录/slices_v7.pdf` - A4 PDF 文档
## 脚本说明
### scripts/slice_processor.py
**核心算法 v7 - 精细平衡版**
- 目标切片高度:1388 像素(按 9:16 比例)
- 搜索范围:±250 像素
- 综合评分:间隙大小 (50 分) + 距离 (30 分) + 高度合理性 (20 分)
- 确保 81% 切片在 1300-1400 像素范围内
**依赖**:
```bash
pip3 install Pillow numpy python-docx
```
### scripts/create_pdf.py
**PDF 生成器 v5**
- 使用 reportlab 库
- 按高度缩放,确保所有切片完整显示
- 左右边距自动计算(相等)
- 右下角页码
**依赖**:
```bash
pip3 install reportlab
```
## 示例对话
**用户**:这张长截图帮我切片,文字不要截断
**助手**:收到,使用长截图切片工具:
1. 分析文字行位置
2. 智能切分(确保拼音完整)
3. 生成 48 个切片 + PDF
---
**用户**:把聊天记录转 PDF,A4 打印
**助手**:可以,生成长截图 PDF:
- A4 尺寸,所有切片完整显示
- 右下角页码
## 版本历史
- **v7**: 精细平衡版,81% 切片在目标范围
- **PDF v5**: 高度优先,左右边距相等,页码右下角
FILE:scripts/create_pdf.py
#!/usr/bin/env python3
"""
从切片图片生成 PDF v5
- A4 大小
- 每页一张切片
- **左右边距固定 2cm**
- **按高度缩放,确保所有切片完整显示**
- 页码在右下角(距右边 1cm,距底部 0.5cm)
使用方法:
python3 create_pdf.py <切片目录> [输出 PDF 路径]
"""
from reportlab.lib.pagesizes import A4
from reportlab.pdfgen import canvas
from reportlab.lib.units import cm
from reportlab.lib.colors import black
import os
import sys
# 配置 - 使用命令行参数
if len(sys.argv) < 2:
print("用法:python3 create_pdf.py <切片目录> [输出 PDF 路径]")
print("示例:python3 create_pdf.py /path/to/slices /path/to/output.pdf")
sys.exit(1)
SLICES_DIR = sys.argv[1]
OUTPUT_PDF = sys.argv[2] if len(sys.argv) > 2 else os.path.join(os.path.dirname(SLICES_DIR), "slices_v7.pdf")
# PDF 页面设置
PAGE_WIDTH, PAGE_HEIGHT = A4 # 21.0cm x 29.7cm
# 边距配置
LEFT_MARGIN = 2 * cm # 左边距固定 2cm
RIGHT_MARGIN = 2 * cm # 右边距固定 2cm
TOP_MARGIN = 1 * cm # 上边距
BOTTOM_MARGIN = 1 * cm # 下边距
# 页码位置(右下角)
# 距页面右边缘 1cm,距页面下边缘 0.5cm
PAGE_NUM_X = PAGE_WIDTH - 1 * cm
PAGE_NUM_Y = 0.5 * cm
def get_slice_files(slices_dir):
files = [f for f in os.listdir(slices_dir) if f.endswith('.jpg')]
files.sort()
return [os.path.join(slices_dir, f) for f in files]
def calculate_scale_for_max_height(slice_files):
"""根据最高切片计算缩放比例,确保所有切片完整显示"""
max_height = 0
for slice_path in slice_files:
img_width, img_height = canvas.ImageReader(slice_path).getSize()
if img_height > max_height:
max_height = img_height
# 可用高度
available_height = PAGE_HEIGHT - TOP_MARGIN - BOTTOM_MARGIN
# 计算缩放比例(让最高切片刚好适应可用高度)
scale = available_height / max_height
return scale, max_height, available_height
def create_pdf(slice_files, output_path):
# 计算缩放比例
scale, max_height, available_height = calculate_scale_for_max_height(slice_files)
# 计算缩放后宽度
scaled_width = 781 * scale
# 计算实际左右边距(居中显示)
actual_side_margin = (PAGE_WIDTH - scaled_width) / 2
print(f"创建 PDF (左右边距 2cm + 高度优先)...")
print(f" 页面大小:A4 ({PAGE_WIDTH/cm:.1f}cm x {PAGE_HEIGHT/cm:.1f}cm)")
print(f" 最高切片:{max_height}px")
print(f" 可用高度:{available_height/cm:.1f}cm")
print(f" 缩放比例:{scale:.4f} cm/px")
print(f" 缩放后宽度:{scaled_width/cm:.1f}cm")
print(f" 实际左右边距:{actual_side_margin/cm:.1f}cm")
print(f" 上边距:{TOP_MARGIN/cm:.1f}cm")
print(f" 下边距:{BOTTOM_MARGIN/cm:.1f}cm")
print(f" 页码位置:右下 (距页面右边缘 1cm, 距下边缘 0.5cm)")
print(f" 切片数量:{len(slice_files)}")
c = canvas.Canvas(output_path, pagesize=A4)
for i, slice_path in enumerate(slice_files):
if i > 0:
c.showPage()
page_num = i + 1
img_width, img_height = canvas.ImageReader(slice_path).getSize()
# 计算缩放后高度
scaled_height = img_height * scale
# 垂直居中
y_position = (PAGE_HEIGHT - scaled_height) / 2
# 水平居中
x_position = (PAGE_WIDTH - scaled_width) / 2
c.drawImage(slice_path, x_position, y_position, width=scaled_width, height=scaled_height)
# 页码(右下角,距页面右边缘 1cm,距下边缘 0.5cm)
c.setFont("Helvetica", 9)
c.setFillColor(black)
page_num_text = f"{page_num} / {len(slice_files)}"
c.drawRightString(PAGE_WIDTH - 1 * cm, 0.5 * cm, page_num_text)
print(f" 第 {page_num} 页:{os.path.basename(slice_path)} ({scaled_width/cm:.1f}cm x {scaled_height/cm:.1f}cm)")
c.save()
print(f" 保存:{output_path}")
def main():
print("=" * 50)
print("切片图片转 PDF v5 - 2cm 边距 + 高度优先")
print("=" * 50)
slice_files = get_slice_files(SLICES_DIR)
if not slice_files:
print(f"错误:未找到切片文件")
return
create_pdf(slice_files, OUTPUT_PDF)
file_size = os.path.getsize(OUTPUT_PDF) / (1024 * 1024)
print("=" * 50)
print("✅ PDF 生成完成!")
print(f" 输出:{OUTPUT_PDF}")
print(f" 大小:{file_size:.1f} MB")
print(f" 页数:{len(slice_files)}")
print("=" * 50)
if __name__ == "__main__":
main()
FILE:scripts/slice_processor.py
#!/usr/bin/env python3
"""
长截图智能切片工具 v7 - 精细平衡版
在目标位置附近小范围搜索,平衡切片高度和间隙质量
使用方法:
python3 slice_processor.py <源图片路径> [输出目录]
"""
import os
import sys
from PIL import Image
import numpy as np
from docx import Document
from docx.shared import Cm
from docx.enum.text import WD_ALIGN_PARAGRAPH
import zipfile
# 配置 - 使用相对路径或命令行参数
SCRIPT_DIR = os.path.dirname(os.path.abspath(__file__))
SKILL_DIR = os.path.dirname(SCRIPT_DIR)
DEFAULT_WORKSPACE = os.path.join(os.path.expanduser("~"), "openclaw/workspace")
# 从命令行参数获取输入输出路径
if len(sys.argv) < 2:
print("用法:python3 slice_processor.py <源图片路径> [输出目录]")
print("示例:python3 slice_processor.py /path/to/source.jpg /path/to/output")
sys.exit(1)
SOURCE_IMG = sys.argv[1]
TASK_DIR = sys.argv[2] if len(sys.argv) > 2 else os.path.join(DEFAULT_WORKSPACE, "temp/slice-task")
SLICES_DIR = os.path.join(TASK_DIR, "slices_v7")
OUTPUT_DOCX = os.path.join(TASK_DIR, "output_v7.docx")
OUTPUT_ZIP = os.path.join(TASK_DIR, "slices_v7.zip")
# 切片配置
TARGET_WIDTH = 781
ASPECT_RATIO = 16/9
INITIAL_SLICE_HEIGHT = int(TARGET_WIDTH * ASPECT_RATIO) # 约 1388
# 高度容差:切片高度应在目标值的±15% 范围内
HEIGHT_TOLERANCE = 0.15
MIN_SLICE_HEIGHT = int(INITIAL_SLICE_HEIGHT * (1 - HEIGHT_TOLERANCE)) # 约 1180
MAX_SLICE_HEIGHT = int(INITIAL_SLICE_HEIGHT * (1 + HEIGHT_TOLERANCE)) # 约 1596
def analyze_content_density(img_path):
"""分析图片内容密度"""
print("分析图片内容密度...")
img = Image.open(img_path).convert('L')
img_array = np.array(img)
height = img_array.shape[0]
row_std = np.std(img_array, axis=1)
# 平滑处理
window = 20
smoothed = np.convolve(row_std, np.ones(window)/window, mode='same')
# 归一化
if smoothed.max() > 0:
smoothed = smoothed / smoothed.max()
return smoothed, img.height
def find_content_regions(smoothed, threshold=0.12):
"""检测内容区域(文字行)"""
is_content = smoothed > threshold
regions = []
in_content = False
region_start = 0
for i, is_text in enumerate(is_content):
if is_text and not in_content:
region_start = i
in_content = True
elif not is_text and in_content:
regions.append((region_start, i))
in_content = False
if in_content:
regions.append((region_start, len(is_content)))
return regions
def find_all_gaps(regions, img_height):
"""找到所有间隙"""
gaps = []
for i in range(len(regions) - 1):
curr_end = regions[i][1]
next_start = regions[i + 1][0]
gap_size = next_start - curr_end
gaps.append({
'start': curr_end,
'end': next_start,
'size': gap_size,
'center': (curr_end + next_start) // 2
})
return gaps
def score_cut_position(proposed_cut, target_height, all_gaps, img_height):
"""
对切分位置打分
综合考虑:间隙大小、与目标的距离、切片高度合理性
"""
best_score = -1000
best_cut = int(proposed_cut)
# 检查所有间隙
for gap in all_gaps:
gap_center = gap['center']
gap_size = gap['size']
# 计算与目标位置的距离
distance = abs(gap_center - proposed_cut)
# 只考虑合理范围内的切分点(±250 像素)
if distance > 250:
continue
# 计算得分
# 1. 间隙大小得分(间隙越大越好,满分 50)
gap_score = min(gap_size / 150 * 50, 50) # 150 像素以上满分
# 2. 距离得分(越近越好,满分 30)
distance_score = (1 - distance / 250) * 30
# 3. 高度合理性得分(在目标范围内最好,满分 20)
resulting_height = gap_center
if MIN_SLICE_HEIGHT <= resulting_height <= MAX_SLICE_HEIGHT:
height_score = 20
else:
# 超出范围则扣分
height_score = max(0, 10 - abs(resulting_height - INITIAL_SLICE_HEIGHT) / 50)
total_score = gap_score + distance_score + height_score
if total_score > best_score:
best_score = total_score
best_cut = gap_center
return best_cut
def find_best_cut_position_v7(start_y, target_height, all_gaps, img_height):
"""
寻找最佳切分位置 v7 - 精细平衡
"""
proposed_bottom = start_y + target_height
if proposed_bottom >= img_height:
return img_height
# 使用评分系统找到最佳切分点
best_cut = score_cut_position(proposed_bottom, target_height, all_gaps, img_height)
return best_cut
def calculate_slice_positions_v7(regions, all_gaps, img_height):
"""计算智能切片位置 v7 - 精细平衡"""
print(f"计算切片位置 (总高度:{img_height})...")
print(f" 检测到 {len(regions)} 个内容区域")
print(f" 检测到 {len(all_gaps)} 个间隙")
print(f" 目标切片高度:{INITIAL_SLICE_HEIGHT} (±{HEIGHT_TOLERANCE*100}%)")
print(f" 允许范围:{MIN_SLICE_HEIGHT} - {MAX_SLICE_HEIGHT}")
slice_positions = []
current_y = 0
slice_num = 0
max_iterations = 200
while current_y < img_height and slice_num < max_iterations:
slice_num += 1
# 寻找最佳切分位置
slice_bottom = find_best_cut_position_v7(
current_y,
INITIAL_SLICE_HEIGHT,
all_gaps,
img_height
)
# 确保至少前进一点
if slice_bottom <= current_y:
slice_bottom = min(current_y + INITIAL_SLICE_HEIGHT // 2, img_height)
slice_height = slice_bottom - current_y
slice_positions.append({
'num': slice_num,
'y_start': current_y,
'y_end': slice_bottom,
'height': slice_height
})
# 标记高度异常的切片
marker = ""
if slice_height < MIN_SLICE_HEIGHT:
marker = " ⚠️ 偏短"
elif slice_height > MAX_SLICE_HEIGHT:
marker = " ⚠️ 偏长"
print(f" 切片 {slice_num}: {current_y} - {slice_bottom} (高度:{slice_height}){marker}")
current_y = slice_bottom
return slice_positions
def create_slices(img_path, slice_positions, output_dir):
"""创建切片图片"""
print(f"创建切片图片...")
os.makedirs(output_dir, exist_ok=True)
img = Image.open(img_path)
slice_files = []
for pos in slice_positions:
slice_img = img.crop((
0,
pos['y_start'],
img.width,
pos['y_end']
))
filename = f"slice_{pos['num']:02d}.jpg"
filepath = os.path.join(output_dir, filename)
slice_img.save(filepath, 'JPEG', quality=95)
slice_files.append(filepath)
print(f" 保存:{filename} ({slice_img.width}x{slice_img.height})")
return slice_files
def create_word_document(slice_files, output_path):
"""创建 Word 文档"""
print(f"创建 Word 文档...")
doc = Document()
section = doc.sections[0]
section.page_height = Cm(29.7)
section.page_width = Cm(21)
section.top_margin = Cm(1)
section.bottom_margin = Cm(1)
section.left_margin = Cm(2)
section.right_margin = Cm(2)
for i, slice_path in enumerate(slice_files):
if i > 0:
doc.add_page_break()
paragraph = doc.add_paragraph()
paragraph.alignment = WD_ALIGN_PARAGRAPH.CENTER
run = paragraph.add_run()
available_width = Cm(21) - Cm(2) - Cm(2)
run.add_picture(slice_path, width=available_width)
doc.save(output_path)
print(f" 保存:{output_path}")
def create_zip(slice_files, output_path):
"""创建 ZIP 包"""
print(f"创建 ZIP 包...")
with zipfile.ZipFile(output_path, 'w', zipfile.ZIP_DEFLATED) as zf:
for filepath in slice_files:
arcname = os.path.basename(filepath)
zf.write(filepath, arcname)
print(f" 保存:{output_path}")
def main():
print("=" * 50)
print("长截图智能切片工具 v7 - 精细平衡版")
print("=" * 50)
if not os.path.exists(SOURCE_IMG):
print(f"错误:源文件不存在 {SOURCE_IMG}")
sys.exit(1)
img = Image.open(SOURCE_IMG)
print(f"源图片:{img.width} x {img.height}")
# 分析内容密度
smoothed, img_height = analyze_content_density(SOURCE_IMG)
# 检测内容区域
regions = find_content_regions(smoothed)
# 找到所有间隙
all_gaps = find_all_gaps(regions, img_height)
# 计算切片位置
slice_positions = calculate_slice_positions_v7(regions, all_gaps, img_height)
print(f"共计划 {len(slice_positions)} 个切片")
# 统计高度分布
heights = [p['height'] for p in slice_positions]
print(f"高度统计:最小={min(heights)}, 最大={max(heights)}, 平均={sum(heights)/len(heights):.0f}")
# 创建切片
slice_files = create_slices(SOURCE_IMG, slice_positions, SLICES_DIR)
# 创建 Word 文档
create_word_document(slice_files, OUTPUT_DOCX)
# 创建 ZIP
create_zip(slice_files, OUTPUT_ZIP)
print("=" * 50)
print("✅ 处理完成!")
print(f" 切片数量:{len(slice_files)}")
print(f" Word 文档:{OUTPUT_DOCX}")
print(f" ZIP 包:{OUTPUT_ZIP}")
print("=" * 50)
if __name__ == "__main__":
main()