我们可以使用 go-fitz 很轻易的制作一款 pdf 文档阅读器,原理是使用 go-fitz 将 pdf 转换为 html 内容,然后使用 go 搭建一个 web 服务,之后再通过浏览器进行文档阅读。相比直接使用浏览器阅读 pdf 文档的好处是原本一些不支持在 pdf 文档中使用的浏览器插件变得可以使用了,我们可以很轻松在阅读 pdf 文档时使用文本翻译,文本语音合成,GPT文章总结……这些功能。
本教程将引导你使用 Go 和 go-fitz 库构建一个 PDF 阅读器。go-fitz 是一个流行的 Go 库,它允许你操作 PDF 文档,包括将它们转换为 HTML。
1. 导入依赖项
首先,你需要导入必要的依赖项:
import (
"flag"
"fmt"
"log"
"net/http"
"os"
"os/signal"
"strconv"
"syscall"
"github.com/gen2brain/go-fitz"
)
flag
:用于处理命令行参数。fmt
:用于格式化和打印输出。log
:用于记录信息。net/http
:用于创建 Web 服务器。os
:用于处理文件。os/signal
:用于处理操作系统信号。strconv
:用于将字符串转换为其他数据类型。syscall
:用于处理系统调用。github.com/gen2brain/go-fitz
:go-fitz 库。
2. 定义常量和模板
接下来,定义一个 HTML 框架和一个常量:
const frame = `
<!DOCTYPE html>
<html>
<head>
<style>
body{background-color:slategray}
div{position:relative;background-color:white;margin:1em auto;box-shadow:1px 1px 8px -2px black}
div#pages{background-color:#0000;margin:0;box-shadow:none}
p{position:absolute;white-space:pre;margin:0}
</style>
</head>
<body>
<div id="pages"></div>
</body>
<script charset="utf-8">
async function load_page(position, page_ix=-1) {
const pages = document.querySelector('#pages');
if (page_ix == -1) {
if (position == 'beforeEnd') {
page_ix = Number(
pages.lastElementChild.id.substr('page'.length)) + 1;
} else if (position == 'afterBegin') {
page_ix = Number(
pages.firstElementChild.id.substr('page'.length)) - 1;
}
}
const url = '/page?ix=' + page_ix;
const resp = await fetch(url);
if (!resp.ok) {
return;
}
const page = await resp.text()
pages.insertAdjacentHTML(position, page);
if (position == 'afterBegin') {
window.scrollTo(0, pages.firstChild.offsetHeight);
}
}
function load_more() {
const autoload = async () => {
if ((window.innerHeight + window.scrollY) >= document.body.parentNode.offsetHeight) {
window.removeEventListener("scroll", autoload);
console.log("loading page to end...");
await load_page('beforeEnd');
window.addEventListener("scroll", autoload);
} else if (window.scrollY == 0) {
window.removeEventListener("scroll", autoload);
console.log("loading page to begin...");
await load_page('afterBegin');
window.addEventListener("scroll", autoload);
}
}
window.addEventListener("scroll", autoload);
}
window.onload = async () => {
const page_ix = (new URLSearchParams(location.search)).get('start') || 0;
await load_page('beforeEnd', page_ix);
await load_page('afterBegin', page_ix - 1);
load_more();
}
</script>
</html>
`
frame
:这是网页的 HTML 框架。它定义了页面的布局、样式和包含 PDF 内容的容器。pages
:这是将 PDF 页面插入的 HTML 容器的 ID。
3. 处理命令行参数
接下来,从命令行参数获取 PDF 文件名:
filename := flag.String("filename", "", "pdf filename")
flag.Parse()
这会获取一个名为 --filename
的命令行参数,它表示要打开的 PDF 文件的路径。
4. 打开 PDF 文档
使用 go-fitz 打开 PDF 文档:
var doc *fitz.Document
doc, err = fitz.New(*filename)
if err != nil {
log.Fatal(err)
}
这会打开给定路径的 PDF 文档并将其存储在 doc
变量中。
5. 处理系统信号
设置一个信号通道来监听 SIGINT
信号(通常是按 Ctrl+C
触发):
signal_chan := make(chan os.Signal, 1)
signal.Notify(signal_chan, syscall.SIGINT)
并设置一个 goroutine 来处理信号并关闭 PDF 文档:
go func() {
<-signal_chan
if doc != nil {
doc.Close()
}
os.Exit(0)
}()
这将确保在收到 SIGINT
信号时关闭 PDF 文档并退出程序。
6. 处理页面请求
设置一个 HTTP 处理程序来处理 /page
路由,它将提供 PDF 的单个页面:
func page_handler(w http.ResponseWriter, r *http.Request) {
values := r.URL.Query()
page_ix, err := strconv.Atoi(values.Get("ix"))
if err != nil {
http.Error(w, "page number valid faild", http.StatusBadRequest)
return
}
log.Println("page ix:", page_ix)
if page_ix < 0 || page_ix >= doc.NumPage() {
http.Error(w, "load page faild", http.StatusNotFound)
return
}
html, err := doc.HTML(page_ix, false)
if err != nil {
http.Error(w, "load page faild", http.StatusInternalServerError)
return
}
fmt.Fprint(w, html)
}
这会获取请求的页面索引 (ix
查询参数),验证索引是否有效,然后使用 go-fitz 将页面转换为 HTML 并发送给客户端。
7. 处理首页请求
设置一个 HTTP 处理程序来处理 /
路由,它将提供带有 PDF 内容的 HTML 框架:
func index_handler(w http.ResponseWriter, r *http.Request) {
fmt.Fprint(w, frame)
}
这会将 frame
框架发送给客户端,其中包含用于加载 PDF 页面的脚本。
8. 启动 Web 服务器
最后,启动一个在端口 8000 上监听的 Web 服务器:
http.HandleFunc("/page", page_handler)
http.HandleFunc("/", index_handler)
log.Fatal(http.ListenAndServe(":8000", nil))
这将启动服务器并允许客户端通过浏览器访问 PDF 内容。
9. 使用 PDF 阅读器
现在,你可以通过访问 http://localhost:8000
在浏览器中打开 PDF 阅读器。该页面将显示 PDF 的第一页。你可以使用滚动条浏览页面,或者使用键盘快捷键(如 ↑
和 ↓
)进行导航。你还可以使用浏览器的插件,如翻译器和语音合成,来增强阅读体验。
完整示例:
package main
import (
"flag"
"fmt"
"log"
"net/http"
"os"
"os/signal"
"strconv"
"syscall"
"github.com/gen2brain/go-fitz"
)
var doc *fitz.Document
const frame = `
<!DOCTYPE html>
<html>
<head>
<style>
body{background-color:slategray}
div{position:relative;background-color:white;margin:1em auto;box-shadow:1px 1px 8px -2px black}
div#pages{background-color:#0000;margin:0;box-shadow:none}
p{position:absolute;white-space:pre;margin:0}
</style>
</head>
<body>
<div id="pages"></div>
</body>
<script charset="utf-8">
async function load_page(position, page_ix=-1) {
const pages = document.querySelector('#pages');
if (page_ix == -1) {
if (position == 'beforeEnd') {
page_ix = Number(
pages.lastElementChild.id.substr('page'.length)) + 1;
} else if (position == 'afterBegin') {
page_ix = Number(
pages.firstElementChild.id.substr('page'.length)) - 1;
}
}
const url = '/page?ix=' + page_ix;
const resp = await fetch(url);
if (!resp.ok) {
return;
}
const page = await resp.text()
pages.insertAdjacentHTML(position, page);
if (position == 'afterBegin') {
window.scrollTo(0, pages.firstChild.offsetHeight);
}
}
function load_more() {
const autoload = async () => {
if ((window.innerHeight + window.scrollY) >= document.body.parentNode.offsetHeight) {
window.removeEventListener("scroll", autoload);
console.log("loading page to end...");
await load_page('beforeEnd');
window.addEventListener("scroll", autoload);
} else if (window.scrollY == 0) {
window.removeEventListener("scroll", autoload);
console.log("loading page to begin...");
await load_page('afterBegin');
window.addEventListener("scroll", autoload);
}
}
window.addEventListener("scroll", autoload);
}
window.onload = async () => {
const page_ix = (new URLSearchParams(location.search)).get('start') || 0;
await load_page('beforeEnd', page_ix);
await load_page('afterBegin', page_ix - 1);
load_more();
}
</script>
</html>
`
func page_handler(w http.ResponseWriter, r *http.Request) {
values := r.URL.Query()
page_ix, err := strconv.Atoi(values.Get("ix"))
if err != nil {
http.Error(w, "page number valid faild", http.StatusBadRequest)
return
}
log.Println("page ix:", page_ix)
if page_ix < 0 || page_ix >= doc.NumPage() {
http.Error(w, "load page faild", http.StatusNotFound)
return
}
html, err := doc.HTML(page_ix, false)
if err != nil {
http.Error(w, "load page faild", http.StatusInternalServerError)
return
}
fmt.Fprint(w, html)
}
func index_handler(w http.ResponseWriter, r *http.Request) {
fmt.Fprint(w, frame)
}
func main() {
filename := flag.String("filename", "", "pdf filename")
flag.Parse()
var err error
doc, err = fitz.New(*filename)
if err != nil {
log.Fatal(err)
}
signal_chan := make(chan os.Signal, 1)
signal.Notify(signal_chan, syscall.SIGINT)
go func() {
<-signal_chan
if doc != nil {
doc.Close()
}
os.Exit(0)
}()
http.HandleFunc("/page", page_handler)
http.HandleFunc("/", index_handler)
log.Fatal(http.ListenAndServe(":8000", nil))
}