""PDF 里的图,难道只能看不能拿?""
—— 别急,今天教你用 Go 写个""图片提取器"",把 PDF 里的图统统打包带走!
你有没有遇到过这种情况:
看到一份超赞的 PDF 报告,里面的图表清晰又专业,想保存下来做参考? 老师发的课件里有张神图,但右键不能保存,复制还糊成马赛克? 想批量提取 PDF 中的所有插图,却只能一张张截图? 别慌!今天我们就用 Go 语言 + pdfcpu 库,手搓一个超实用的 PDF 图片提取工具,一键把 PDF 里的所有图片“扒”出来,整齐码好,命名清晰,绝不手软!
🛠️ 它是怎么工作的? 简单来说,这个工具干了三件事:
读 PDF:用 pdfcpu 库解析 PDF 文件结构。 找图片:遍历每一页,找出所有嵌入的图像对象(不管是 JPG、PNG 还是其他格式)。 存图片:把每张图按 名称-页码-对象ID.格式 的规则命名,存到你指定的文件夹里。 💡 小知识:PDF 里的图片其实是“嵌入对象”,每个都有唯一 ID 和所属页码。我们就是靠这些信息精准定位每张图!
😎 用起来有多爽? 假设你有个文件叫 report.pdf,只需一行命令:
extractimages report.pdf
它就会自动创建一个叫 report.pdf.images 的文件夹,里面是这样的:
chart-5-42.jpg
diagram-12-108.png
image-3-17.jpg
...
想换个目录?加个 -o 就行:
extractimages -o my_pics report.pdf
连帮助都贴心准备好了:
extractimages --help
🧙♂️ 技术小彩蛋 用 unsafe “偷看”私有字段:pdfcpu.Image 的内部字段是私有的,但我们用 unsafe.Pointer 强行转成自定义结构体 MyImage,就能拿到图片名、页码、对象ID等关键信息!(别怕,这在 Go 里是合法“黑科技”) 自动防重名:如果文件名冲突,自动加上时间戳,绝不覆盖! 智能默认输出目录:不指定 -o?没问题,自动用 PDF文件名.images 当文件夹! 🚀 快来试试吧! 只要你会装 Go,三步搞定:
安装依赖:
go mod init extractimages
go get github.com/pdfcpu/pdfcpu
把下面的代码保存为 main.go
编译运行:
go build -o extractimages main.go
./extractimages your_file.pdf
从此,PDF 再也不是图片的"监狱"——而是你的"图库仓库"!
📜 完整代码(直接复制就能用!)
package main
import (
"flag" // 用于处理命令行参数
"fmt" // 用于格式化输出
"io" // 提供基础的I/O接口
"log" // 提供日志功能
"os" // 提供操作系统功能接口
"path/filepath" // 用于处理文件路径
"strings" // 提供字符串处理函数
"time" // 用于计时和时间戳
"unsafe" // 用于不安全的内存操作
"github.com/pdfcpu/pdfcpu/pkg/api" // pdfcpu库的API接口
"github.com/pdfcpu/pdfcpu/pkg/pdfcpu" // pdfcpu库的核心功能
)
// MyImage 是对pdfcpu.Image的封装,用于访问图片的详细信息
type MyImage struct {
io.Reader // 嵌入的Reader接口,用于读取图片数据
Name string // 图片名称
FileType string // 文件类型(如jpg、png等)
PageNr int // 图片所在的PDF页码
ObjNr int // 图片在PDF中的对象编号
}
// 全局变量,用于统计提取的图片数量
var extractedImagesCount int
// main 程序入口函数
func main() {
// 设置日志格式
log.SetFlags(log.Ldate | log.Ltime | log.Lshortfile)
log.Println("PDF图片提取工具启动")
// 解析命令行参数
var help bool
var outputDir string
flag.BoolVar(&help, "h", false, "显示帮助信息")
flag.BoolVar(&help, "help", false, "显示帮助信息")
flag.StringVar(&outputDir, "o", "", "指定输出图片的目录")
flag.StringVar(&outputDir, "output", "", "指定输出图片的目录")
flag.Parse()
// 获取剩余的非选项参数(即PDF文件路径)
args := flag.Args()
if help || len(args) < 1 {
showHelp()
return
}
// 获取PDF文件的绝对路径
pdfFilename, err := filepath.Abs(args[0])
if err != nil {
log.Fatalf("获取文件绝对路径失败: %v", err)
}
// 检查文件是否存在
if _, err := os.Stat(pdfFilename); os.IsNotExist(err) {
log.Fatalf("PDF文件不存在: %s", pdfFilename)
}
// 检查文件扩展名是否为pdf
if !strings.HasSuffix(strings.ToLower(pdfFilename), ".pdf") {
log.Printf("警告: 文件扩展名不是.pdf,可能不是有效的PDF文件: %s\n", pdfFilename)
}
// 确定输出目录
if outputDir == "" {
// 如果未指定输出目录,使用默认目录(PDF文件名.images)
pdfName := strings.TrimSuffix(filepath.Base(pdfFilename), ".pdf")
outputDir = pdfName + ".images"
}
// 获取输出目录的绝对路径
outputDir, err = filepath.Abs(outputDir)
if err != nil {
log.Fatalf("获取输出目录绝对路径失败: %v", err)
}
// 创建输出目录(如果不存在)
if err := os.MkdirAll(outputDir, 0755); err != nil {
log.Fatalf("创建输出目录失败: %v", err)
}
log.Printf("开始处理PDF文件: %s", pdfFilename)
log.Printf("图片将保存到目录: %s", outputDir)
startTime := time.Now()
// 打开 PDF 文件
pdfFile, err := os.Open(pdfFilename)
if err != nil {
log.Fatalf("打开PDF文件失败: %v", err)
}
// 确保程序结束时关闭文件
defer pdfFile.Close()
// 解析 PDF,将提取的图片写入指定目录
// 使用saveImageToDir函数作为回调函数处理每个提取到的图片
if err := api.ExtractImages(pdfFile, nil, saveImageToDir(outputDir), nil); err != nil {
log.Fatalf("提取图片过程中出错: %v", err)
}
elapsedTime := time.Since(startTime)
log.Printf("图片提取完成,共提取 %d 张图片,耗时: %v", extractedImagesCount, elapsedTime)
log.Printf("所有图片已保存到目录: %s", outputDir)
}
// showHelp 显示帮助信息
func showHelp() {
fmt.Println("PDF图片提取工具 - 从PDF文件中提取所有图片并保存到指定目录")
fmt.Println("\n用法:")
fmt.Println(" extractimages [选项] <PDF文件路径>")
fmt.Println("\n选项:")
fmt.Println(" -h, --help 显示此帮助信息")
fmt.Println(" -o, --output <目录路径> 指定输出图片的目录(默认: PDF文件名.images)")
fmt.Println("\n示例:")
fmt.Println(" extractimages document.pdf")
fmt.Println(" extractimages -o images_folder document.pdf")
fmt.Println("\n说明:")
fmt.Println(" 1. 程序会从指定的PDF文件中提取所有图片")
fmt.Println(" 2. 提取的图片将保存到指定目录或默认目录中")
fmt.Println(" 3. 图片文件名格式: 图片名称-页码-对象编号.文件类型")
fmt.Println(" 4. 如果指定的目录不存在,将自动创建")
}
// saveImageToDir 创建一个回调函数,用于将提取的图片保存到指定目录
// 参数 outputDir 是保存图片的目标目录路径
// 返回值是一个函数,该函数将在pdfcpu库提取到图片时被调用
func saveImageToDir(outputDir string) func(pdfcpu.Image, bool, int) error {
return func(img pdfcpu.Image, singleImgPerPage bool, maxPageDigits int) error {
// 使用unsafe包将pdfcpu.Image类型强制转换为MyImage类型,以便访问私有字段
myImg := (*MyImage)(unsafe.Pointer(&img))
// 处理空文件名的情况
name := myImg.Name
if name == "" {
name = "image"
}
// 处理未知文件类型的情况
fileType := myImg.FileType
if fileType == "" {
fileType = "jpg" // 默认使用jpg格式
}
// 生成图片文件名,格式为:图片名称-页码-对象编号.文件类型
filename := fmt.Sprintf("%s-%d-%d.%s", name, myImg.PageNr, myImg.ObjNr, fileType)
// 生成完整的文件路径
filePath := filepath.Join(outputDir, filename)
// 检查文件是否已存在,如果存在则添加时间戳
if _, err := os.Stat(filePath); err == nil {
timestamp := time.Now().Format("20060102_150405")
filename = fmt.Sprintf("%s-%d-%d_%s.%s", name, myImg.PageNr, myImg.ObjNr, timestamp, fileType)
filePath = filepath.Join(outputDir, filename)
log.Printf("图片文件已存在,将使用新文件名: %s", filename)
}
// 创建目标文件
outFile, err := os.Create(filePath)
if err != nil {
log.Printf("创建图片文件失败: %v", err)
return err
}
// 确保文件关闭
defer outFile.Close()
// 将图片数据写入文件
n, err := io.Copy(outFile, img.Reader)
if err != nil {
log.Printf("写入图片数据失败: %v", err)
return err
}
// 增加统计计数
extractedImagesCount++
// 输出成功处理的图片文件名和大小
log.Printf("成功提取图片 #%d: %s (大小: %d 字节)", extractedImagesCount, filename, n)
return nil
}
}
🎉 从此 PDF 里的图,都是你的!
快去试试吧,说不定下一个被你提取到的,就是价值千金的设计图呢~
往期部分文章列表
- 200KB 的烦恼,Go 语言 20 分钟搞定!—— 一个程序员的图片压缩自救指南
- 从“CPU 烧开水“到优雅暂停:Go 里 sync.Cond 的正确打开方式
- 时移世易,篡改天机:吾以 Go 语令 Windows 文件“返老还童“记
- golang圆阵列图记:天灵灵地灵灵图标排圆形
- golang解图记
- 从 4.8 秒到 0.25 秒:我是如何把 Go 正则匹配提速 19 倍的?
- 用 Go 手搓一个内网 DNS 服务器:从此告别 IP 地址,用域名畅游家庭网络!
- 我用Go写了个华容道游戏,曹操终于不用再求关羽了!
- 用 Go 接口把 Excel 变成数据库:一个疯狂但可行的想法
- 穿墙术大揭秘:用 Go 手搓一个"内网穿透"神器!
- 布隆过滤器(go):一个可能犯错但从不撒谎的内存大师
- 自由通讯的魔法:Go从零实现UDP/P2P 聊天工具
- Go语言实现的简易远程传屏工具:让你的屏幕「飞」起来
- 当你的程序学会了"诈尸":Go 实现 Windows 进程守护术
- 验证码识别API:告别收费接口,迎接免费午餐
- 用 Go 给 Windows 装个"顺风耳":两分钟写个录音小工具
- 无奈!我用go写了个MySQL服务
- 使用 Go + govcl 实现 Windows 资源管理器快捷方式管理器
- 用 Go 手搓一个 NTP 服务:从"时间混乱"到"精准同步"的奇幻之旅
- 用 Go 手搓一个 Java 构建工具:当 IDE 不在身边时的自救指南
- 深入理解 Windows 全局键盘钩子(Hook):拦截 Win 键的 Go 实现
- 用 Go 语言实现《周易》大衍筮法起卦程序
- Go 语言400行代码实现 INI 配置文件解析器:支持注释、转义与类型推断
- 高性能 Go 语言带 TTL 的内存缓存实现:精确过期、自动刷新、并发安全