mirror of
https://github.com/lixiaofei123/pdftoolbox.git
synced 2026-08-12 21:21:46 +08:00
init
This commit is contained in:
@@ -0,0 +1,33 @@
|
||||
name: ci
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- 'master'
|
||||
|
||||
jobs:
|
||||
docker:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
-
|
||||
name: Checkout
|
||||
uses: actions/checkout@v2
|
||||
-
|
||||
name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v1
|
||||
-
|
||||
name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v1
|
||||
-
|
||||
name: Login to DockerHub
|
||||
uses: docker/login-action@v1
|
||||
with:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
-
|
||||
name: Build and push
|
||||
uses: docker/build-push-action@v2
|
||||
with:
|
||||
context: .
|
||||
push: true
|
||||
tags: mrlee326/pdftoolbox:latest
|
||||
@@ -0,0 +1,4 @@
|
||||
pdftoolbox*
|
||||
input/
|
||||
output/
|
||||
__debug_bin
|
||||
+18
@@ -0,0 +1,18 @@
|
||||
FROM golang AS build
|
||||
WORKDIR /build
|
||||
COPY . .
|
||||
ENV GOPROXY https://goproxy.io,direct
|
||||
ENV CGO_ENABLED=0
|
||||
RUN go build -o pdftoolbox
|
||||
|
||||
FROM ubuntu
|
||||
RUN apt-get -y update && rm -rf /var/lib/apt/lists/*
|
||||
RUN cp gs /usr/bin/gs
|
||||
RUN mkdir -p /opt/pdftoolbox
|
||||
RUN mkdir -p /opt/pdftoolbox/input
|
||||
RUN mkdir -p /opt/pdftoolbox/output
|
||||
COPY --from=build /build/pdftoolbox /opt/pdftoolbox/pdftoolbox
|
||||
COPY static /opt/pdftoolbox/static
|
||||
EXPOSE 8082
|
||||
WORKDIR /opt/pdftoolbox/
|
||||
ENTRYPOINT ["./pdftoolbox"]
|
||||
@@ -0,0 +1,34 @@
|
||||
|
||||
[](https://github.com/lixiaofei123/pdftoolbox/actions/workflows/docker.yml)
|
||||
|
||||
## 一个简单的在线压缩pdf网站
|
||||
|
||||
使用 [GhostScript](https://www.ghostscript.com) 进行压缩
|
||||
|
||||
## 使用方法
|
||||
|
||||
### 使用Docker部署
|
||||
|
||||
建议使用Docker一键部署
|
||||
|
||||
```
|
||||
docker run -d --name pdftoolbox --restart=always -p 8082:8082 -v /data/pdftoolbox/input:/opt/pdftoolbox/input -v /data/pdftoolbox/output:/opt/pdftoolbox/output mrlee326/pdftoolbox
|
||||
```
|
||||
|
||||
启动成功后,在浏览器中访问 http://ip:8082,如下图所示
|
||||
|
||||

|
||||
|
||||
点击【点击此处上传】按钮,选择要转换的文件,即可上传。目前支持三种压缩质量
|
||||
- 高质量 (300dpi)
|
||||
- 中质量 (150dpi)
|
||||
- 低质量 (72dpi)
|
||||
|
||||
### 在Linux上部署
|
||||
|
||||
请参考Dockerfile文件
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
module github.com/lixiaofei123/pdftoolbox
|
||||
|
||||
go 1.17
|
||||
|
||||
require github.com/google/uuid v1.3.0
|
||||
@@ -0,0 +1,2 @@
|
||||
github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I=
|
||||
github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
@@ -0,0 +1,106 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os/exec"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type HandleStatus int
|
||||
type CompressSetting string
|
||||
|
||||
const (
|
||||
Ready HandleStatus = iota
|
||||
Start
|
||||
Compressing
|
||||
Success
|
||||
Error
|
||||
)
|
||||
|
||||
const (
|
||||
Prepress CompressSetting = "prepress"
|
||||
Ebook CompressSetting = "ebook"
|
||||
Screen CompressSetting = "screen"
|
||||
)
|
||||
|
||||
const (
|
||||
English string = "eng"
|
||||
SimplifiedChinese string = "chi_sim+chi_sim_vert"
|
||||
TraditionalChinese string = "chi_tra+chi_tra_vert"
|
||||
Japanse string = "jpn+jpn_vert"
|
||||
korea string = "kor+kor_vert"
|
||||
)
|
||||
|
||||
var DefaultOCRLanguages []string = []string{English, SimplifiedChinese}
|
||||
|
||||
type CommandWriter struct {
|
||||
totalPage int
|
||||
handlePage int
|
||||
progress HandleProgress
|
||||
state HandleStatus
|
||||
}
|
||||
|
||||
func NewCommandWriter(progress HandleProgress) *CommandWriter {
|
||||
return &CommandWriter{
|
||||
totalPage: 0,
|
||||
handlePage: 0,
|
||||
progress: progress,
|
||||
}
|
||||
}
|
||||
|
||||
func (w *CommandWriter) Write(p []byte) (n int, err error) {
|
||||
|
||||
output := string(p)
|
||||
lines := strings.Split(output, "\n")
|
||||
for _, line := range lines {
|
||||
fmt.Println(line)
|
||||
if strings.HasPrefix(line, "Processing pages") {
|
||||
w.totalPage, _ = strconv.Atoi(line[strings.Index(line, "through ")+8 : len(line)-1])
|
||||
w.progress(float32(1)/float32(w.totalPage), Start, "")
|
||||
} else if strings.HasPrefix(line, "Page ") {
|
||||
w.handlePage, _ = strconv.Atoi(line[5:])
|
||||
w.progress(float32(w.handlePage)/float32(w.totalPage), Compressing, "")
|
||||
} else if strings.Contains(line, "error") && w.state != Error {
|
||||
reason := line[strings.Index(line, "error")+6:]
|
||||
w.progress(100, Error, reason)
|
||||
w.state = Error
|
||||
}
|
||||
}
|
||||
|
||||
return len(p), nil
|
||||
}
|
||||
|
||||
type HandleProgress func(progress float32, status HandleStatus, reason string)
|
||||
|
||||
func CompressPdf(inputFile string, outputFile string, setting CompressSetting, progress HandleProgress) {
|
||||
cmd := exec.Command("/usr/bin/gs", "-sDEVICE=pdfwrite", "-dCompatibilityLevel=1.4", fmt.Sprintf("-dPDFSETTINGS=/%s", string(setting)),
|
||||
"-dNOPAUSE", "-dBATCH", fmt.Sprintf("-sOutputFile=%s", outputFile), inputFile)
|
||||
|
||||
commandWriter := NewCommandWriter(progress)
|
||||
cmd.Stdout = commandWriter
|
||||
cmd.Stderr = commandWriter
|
||||
|
||||
err := cmd.Run()
|
||||
if err == nil {
|
||||
progress(1, Success, "")
|
||||
}
|
||||
}
|
||||
|
||||
func OCRPdf(inputFile string, outputFile string, languages []string, progress HandleProgress) {
|
||||
|
||||
cmd := exec.Command("/usr/bin/gs", "-sDEVICE=ocr", fmt.Sprintf(`-sOCRLanguage="%s"`, strings.Join(append(DefaultOCRLanguages, languages...), "+")), "-o", outputFile, "-r600", "-dDownScaleFactor=3", inputFile)
|
||||
|
||||
commandWriter := NewCommandWriter(progress)
|
||||
cmd.Stdout = commandWriter
|
||||
cmd.Stderr = commandWriter
|
||||
|
||||
err := cmd.Run()
|
||||
if err == nil {
|
||||
progress(1, Success, "")
|
||||
} else {
|
||||
if commandWriter.state != Error {
|
||||
progress(0, Error, err.Error())
|
||||
}
|
||||
}
|
||||
}
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 29 KiB |
@@ -0,0 +1,235 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"net/http"
|
||||
"os"
|
||||
"path"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
var taskService *TaskService
|
||||
|
||||
func init() {
|
||||
taskService = NewTaskService()
|
||||
}
|
||||
|
||||
func HandleError(err error, rw http.ResponseWriter) {
|
||||
rw.WriteHeader(503)
|
||||
rw.Write([]byte(err.Error()))
|
||||
}
|
||||
|
||||
func CompressFile(rw http.ResponseWriter, r *http.Request) {
|
||||
|
||||
r.ParseForm()
|
||||
|
||||
file, handler, err := r.FormFile("file")
|
||||
if err != nil {
|
||||
HandleError(err, rw)
|
||||
return
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
setting := r.FormValue("setting")
|
||||
if setting == "" {
|
||||
setting = "prepress"
|
||||
}
|
||||
|
||||
fileBytes, err := ioutil.ReadAll(file)
|
||||
if err != nil {
|
||||
HandleError(err, rw)
|
||||
return
|
||||
}
|
||||
uuid := uuid.NewString()
|
||||
|
||||
inputFile := fmt.Sprintf("input/%s_%s", uuid[:6], handler.Filename)
|
||||
err = ioutil.WriteFile(inputFile, fileBytes, 0666)
|
||||
if err != nil {
|
||||
HandleError(err, rw)
|
||||
return
|
||||
}
|
||||
|
||||
outFile := fmt.Sprintf("output/%s_%s_%s.txt", setting, uuid[:6], handler.Filename)
|
||||
|
||||
go func(uuid string, dataLen int) {
|
||||
|
||||
defer func() {
|
||||
if err := recover(); err != nil {
|
||||
log.Println(err)
|
||||
}
|
||||
}()
|
||||
|
||||
CompressPdf(inputFile, outFile, CompressSetting(setting), func() HandleProgress {
|
||||
|
||||
task := &Task{
|
||||
ID: uuid,
|
||||
TaskType: Compress,
|
||||
Name: path.Base(inputFile),
|
||||
OutName: path.Base(outFile),
|
||||
CreateTime: time.Now().Unix(),
|
||||
Status: Ready,
|
||||
Progress: 0,
|
||||
Size: int64(dataLen),
|
||||
}
|
||||
|
||||
taskId := taskService.AddTask(task)
|
||||
|
||||
return func(progress float32, status HandleStatus, reason string) {
|
||||
if status == Start {
|
||||
taskService.UpdateTask(taskId, Start, progress, "")
|
||||
} else if status == Compressing {
|
||||
taskService.UpdateTask(taskId, Compressing, progress, "")
|
||||
} else if status == Success {
|
||||
fi, _ := os.Stat(outFile)
|
||||
taskService.UpdateTaskFileSize(taskId, fi.Size())
|
||||
} else if status == Error {
|
||||
taskService.UpdateTask(taskId, Error, progress, reason)
|
||||
}
|
||||
}
|
||||
}())
|
||||
|
||||
}(uuid, len(fileBytes))
|
||||
|
||||
rw.WriteHeader(200)
|
||||
rw.Write([]byte(fmt.Sprintf(`{"taskID" : "%s"}`, uuid)))
|
||||
|
||||
}
|
||||
|
||||
func OCRFile(rw http.ResponseWriter, r *http.Request) {
|
||||
|
||||
r.ParseForm()
|
||||
|
||||
file, handler, err := r.FormFile("file")
|
||||
if err != nil {
|
||||
HandleError(err, rw)
|
||||
return
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
languages := []string{}
|
||||
languagesStr := r.FormValue("languages")
|
||||
if languagesStr != "" {
|
||||
languages = strings.Split(languagesStr, ",")
|
||||
}
|
||||
|
||||
fileBytes, err := ioutil.ReadAll(file)
|
||||
if err != nil {
|
||||
HandleError(err, rw)
|
||||
return
|
||||
}
|
||||
uuid := uuid.NewString()
|
||||
|
||||
inputFile := fmt.Sprintf("input/%s_%s", uuid[:6], handler.Filename)
|
||||
err = ioutil.WriteFile(inputFile, fileBytes, 0666)
|
||||
if err != nil {
|
||||
HandleError(err, rw)
|
||||
return
|
||||
}
|
||||
|
||||
outFile := fmt.Sprintf("output/%s_%s.txt", uuid[:6], handler.Filename)
|
||||
|
||||
go func(uuid string, dataLen int) {
|
||||
|
||||
defer func() {
|
||||
if err := recover(); err != nil {
|
||||
log.Println(err)
|
||||
}
|
||||
}()
|
||||
|
||||
OCRPdf(inputFile, outFile, languages, func() HandleProgress {
|
||||
|
||||
task := &Task{
|
||||
ID: uuid,
|
||||
TaskType: OCR,
|
||||
Name: path.Base(inputFile),
|
||||
OutName: path.Base(outFile),
|
||||
CreateTime: time.Now().Unix(),
|
||||
Status: Ready,
|
||||
Progress: 0,
|
||||
Size: int64(dataLen),
|
||||
}
|
||||
|
||||
taskId := taskService.AddTask(task)
|
||||
|
||||
return func(progress float32, status HandleStatus, reason string) {
|
||||
if status == Start {
|
||||
taskService.UpdateTask(taskId, Start, progress, "")
|
||||
} else if status == Compressing {
|
||||
taskService.UpdateTask(taskId, Compressing, progress, "")
|
||||
} else if status == Success {
|
||||
fi, _ := os.Stat(outFile)
|
||||
taskService.UpdateTaskFileSize(taskId, fi.Size())
|
||||
} else if status == Error {
|
||||
taskService.UpdateTask(taskId, Error, progress, reason)
|
||||
}
|
||||
}
|
||||
}())
|
||||
|
||||
}(uuid, len(fileBytes))
|
||||
|
||||
rw.WriteHeader(200)
|
||||
rw.Write([]byte(fmt.Sprintf(`{"taskID" : "%s"}`, uuid)))
|
||||
|
||||
}
|
||||
|
||||
func GetTask(rw http.ResponseWriter, r *http.Request) {
|
||||
r.ParseForm()
|
||||
taskId := r.FormValue("taskId")
|
||||
task, err := taskService.GetTask(taskId)
|
||||
if err != nil {
|
||||
HandleError(err, rw)
|
||||
return
|
||||
}
|
||||
|
||||
data, _ := json.Marshal(task)
|
||||
rw.Write(data)
|
||||
}
|
||||
|
||||
func Download(rw http.ResponseWriter, r *http.Request) {
|
||||
r.ParseForm()
|
||||
taskId := r.FormValue("taskId")
|
||||
task, err := taskService.GetTask(taskId)
|
||||
if err != nil {
|
||||
HandleError(err, rw)
|
||||
return
|
||||
}
|
||||
|
||||
data, err := ioutil.ReadFile(fmt.Sprintf("output/%s", task.OutName))
|
||||
if err != nil {
|
||||
HandleError(err, rw)
|
||||
return
|
||||
}
|
||||
|
||||
rw.Header().Add("Content-Length", strconv.Itoa(len(data)))
|
||||
rw.Header().Add("Content-Type", "application/octet-stream")
|
||||
rw.Header().Add("Content-Disposition", fmt.Sprintf(`attachment; filename="%s"`, task.OutName))
|
||||
rw.Write(data)
|
||||
}
|
||||
|
||||
func Tasks(rw http.ResponseWriter, r *http.Request) {
|
||||
tasks := taskService.GetTaskList()
|
||||
data, _ := json.Marshal(tasks)
|
||||
rw.Write(data)
|
||||
}
|
||||
|
||||
func main() {
|
||||
|
||||
http.HandleFunc("/api/compress", CompressFile)
|
||||
http.HandleFunc("/api/ocr", OCRFile)
|
||||
http.HandleFunc("/api/task", GetTask)
|
||||
http.HandleFunc("/api/tasks", Tasks)
|
||||
http.HandleFunc("/download", Download)
|
||||
fs := http.FileServer(http.Dir("./static"))
|
||||
http.Handle("/", fs)
|
||||
|
||||
log.Println("服务已经启动......")
|
||||
log.Fatal(http.ListenAndServe(":8082", nil))
|
||||
|
||||
}
|
||||
Executable
BIN
Binary file not shown.
@@ -0,0 +1,83 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1.0">
|
||||
<title>PDF在线工具箱</title>
|
||||
<link rel="stylesheet" type="text/css" href="/upload.css" />
|
||||
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div id="fullscreen">
|
||||
<div id="nav">
|
||||
PDF在线工具箱
|
||||
</div>
|
||||
<div id="container">
|
||||
<div id="noscrollbar">
|
||||
<div class="checkboxGroup">
|
||||
<div class="checkbox menu active" value="compress">PDF压缩</div>
|
||||
<div class="checkbox menu" value="ocr">PDF文本提取</div>
|
||||
</div>
|
||||
<div id="upload-area">
|
||||
<div id="upload-box">
|
||||
<div class="upload-icon">
|
||||
<svg class="icon" viewBox="0 0 1024 1024" width="80" height="80" version="1.1"
|
||||
xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
|
||||
<path fill="#eee"
|
||||
d="M796.2 466.4c0-2.4 0.4-4.8 0.4-7.2 0-130-103.6-235.2-231.4-235.2-92.2 0-171.4 54.8-208.6 134-16.2-8.2-34.4-13-53.6-13-59 0-108.2 43.8-117.6 101C114.6 470.4 64 538.2 64 618c0 100.4 80.2 182 179 182L448 800l0-160-96.4 0 160.4-167.4 160.4 167.2-96.4 0 0 160 220.6 0c90.4 0 163.4-75 163.4-166.8C960 541.2 886.6 466.6 796.2 466.4z">
|
||||
</path>
|
||||
</svg>
|
||||
<br>
|
||||
<span class="upload-tips">点击此处上传</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="allow-upload-tips">
|
||||
<div id="selectQuality">
|
||||
<div class="checkboxGroup">
|
||||
<span>请选择压缩质量</span>
|
||||
<div class="checkbox quality" value="prepress">高质量(300 dpi)</div>
|
||||
<div class="checkbox quality active" value="ebook">中等质量(150 dpi)</div>
|
||||
<div class="checkbox quality" value="screen">低质量(72dpi)</div>
|
||||
</div>
|
||||
</div>
|
||||
<div id="selectLanguage" style="display: none;">
|
||||
<div class="checkboxGroup">
|
||||
<span>请选择额外语言</span>
|
||||
<div class="checkbox language" value="chi_tra+chi_tra_vert">繁体中文</div>
|
||||
<div class="checkbox language" value="jpn+jpn_vert">日文</div>
|
||||
<div class="checkbox language" value="kor+kor_vert">韩语</div>
|
||||
</div>
|
||||
<div class="tips">已经默认支持简体中文和英文,额外选择语言将会降低转换速度</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
</div>
|
||||
<div id="upload-files">
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
<div id="foot">
|
||||
<div class="foot-part foot-left" id="legalStatement">
|
||||
|
||||
</div>
|
||||
<div class="foot-part foot-center">
|
||||
<span id="icp" style="display: none;"></span>
|
||||
Power By <a href="https://www.lixf.cc" target="_blank">李小飞</a>
|
||||
</div>
|
||||
<div class="foot-part foot-right">
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<script src="/upload.js"></script>
|
||||
|
||||
</body>
|
||||
|
||||
</html>
|
||||
@@ -0,0 +1,306 @@
|
||||
#fullscreen {
|
||||
position: fixed;
|
||||
top: 0px;
|
||||
bottom: 0px;
|
||||
left: 0px;
|
||||
right: 0px;
|
||||
background: #E4E4E4;
|
||||
background-size: cover;
|
||||
background-position: center;
|
||||
}
|
||||
|
||||
body {
|
||||
padding: 0px;
|
||||
margin: 0px;
|
||||
}
|
||||
|
||||
#nav {
|
||||
width: calc(100% - 40px);
|
||||
padding: 15px 20px;
|
||||
background: white;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
img {
|
||||
padding: 0px;
|
||||
margin: 0px;
|
||||
}
|
||||
|
||||
#userinfo {
|
||||
float: right;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.buttons {
|
||||
display: inline-block;
|
||||
margin-right: 20px;
|
||||
}
|
||||
|
||||
.buttons a {
|
||||
color: #666666;
|
||||
text-decoration: none;
|
||||
font-size: 14px;
|
||||
margin-right: 10px;
|
||||
}
|
||||
|
||||
.buttons a:hover {
|
||||
font-size: 15px;
|
||||
}
|
||||
|
||||
#avatar {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
border-radius: 20px;
|
||||
overflow: hidden;
|
||||
cursor: pointer;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
#container {
|
||||
width: calc(100% - 20px);
|
||||
max-width: 1320px;
|
||||
height: calc(100% - 100px);
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
#noscrollbar {
|
||||
padding-top: 20px;
|
||||
width: calc(100% + 17px);
|
||||
height: calc(100% - 120px);
|
||||
overflow-y: scroll;
|
||||
overflow-x: hidden;
|
||||
}
|
||||
|
||||
#upload-area {
|
||||
background: white;
|
||||
padding: 40px 40px 20px 40px;
|
||||
border-radius: 5px;
|
||||
color: #666666;
|
||||
}
|
||||
|
||||
#upload-box {
|
||||
width: 100%;
|
||||
height: 240px;
|
||||
border: 1px solid rgb(212, 212, 212);
|
||||
border-radius: 5px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
#upload-box:hover {
|
||||
border: 1px solid #409EFF;
|
||||
}
|
||||
|
||||
.upload-icon {
|
||||
text-align: center;
|
||||
padding-top: 60px;
|
||||
}
|
||||
|
||||
.upload-tips {
|
||||
margin-top: 10px;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.allow-upload-tips {
|
||||
margin-top: 10px;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
#upload-files {
|
||||
margin-top: 20px;
|
||||
display: flex;
|
||||
flex-wrap: wrap
|
||||
}
|
||||
|
||||
.upload-file-card {
|
||||
width: calc(50% - 12px);
|
||||
height: 123px;
|
||||
margin: 6px;
|
||||
flex-shrink: 1;
|
||||
background: white;
|
||||
position: relative;
|
||||
cursor: pointer;
|
||||
float: left;
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.upload-file-thumb {
|
||||
width: 160px;
|
||||
flex-shrink: 0;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.file-links-box {
|
||||
flex: 1;
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
.download_button {
|
||||
display: block;
|
||||
width: 100px;
|
||||
padding: 5px 10px;
|
||||
background: #069af1;
|
||||
color: white;
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
text-align: center;
|
||||
margin-top: 10px;
|
||||
font-size: 14px;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.download_button:hover{
|
||||
background: #0588d4;
|
||||
}
|
||||
|
||||
.fileinfo{
|
||||
display: block;
|
||||
margin-top: 10px;
|
||||
font-size: 12px;
|
||||
text-align: center;
|
||||
color: grey;
|
||||
}
|
||||
|
||||
.link {
|
||||
width: calc(100% - 24px);
|
||||
margin-bottom: 6px;
|
||||
padding: 6px 10px;
|
||||
font-size: 14px;
|
||||
cursor: pointer;
|
||||
border: 1px solid grey;
|
||||
background: #eee;
|
||||
}
|
||||
|
||||
.copytip {
|
||||
display: inline-block;
|
||||
position: absolute;
|
||||
left: 4px;
|
||||
top: 5px;
|
||||
z-index: 1;
|
||||
padding: 2px 8px;
|
||||
background: #61cc00bd;
|
||||
color: white;
|
||||
border-radius: 2px;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.upload-progress {
|
||||
width: 100%;
|
||||
position: absolute;
|
||||
bottom: 0px;
|
||||
}
|
||||
|
||||
.upload-error-cover {
|
||||
position: absolute;
|
||||
top: 0px;
|
||||
bottom: 5px;
|
||||
left: 0px;
|
||||
right: 0px;
|
||||
z-index: 1;
|
||||
background: rgba(0, 0, 0, 0.5);
|
||||
padding: 15px 5px;
|
||||
color: white;
|
||||
text-align: center;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.progress-bar {
|
||||
height: 3px;
|
||||
background-color: #409EFF;
|
||||
}
|
||||
|
||||
#foot {
|
||||
position: fixed;
|
||||
left: 0px;
|
||||
right: 0px;
|
||||
bottom: 0px;
|
||||
padding: 10px 20px;
|
||||
background-color: white;
|
||||
display: flex;
|
||||
justify-content: space-around;
|
||||
color: #666666;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
#foot a {
|
||||
color: #666666;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.foot-part {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.foot-left {
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.foot-center {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.foot-right {
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.checkboxGroup {
|
||||
display: flex;
|
||||
margin-top: 15px;
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
.checkboxGroup .checkbox {
|
||||
padding: 5px 20px;
|
||||
cursor: pointer;
|
||||
border: 1px solid rgb(235, 235, 235);
|
||||
margin-left: 15px;
|
||||
}
|
||||
|
||||
.checkboxGroup .checkbox:hover {
|
||||
background: #409EFF;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.menu{
|
||||
padding: 10px 20px;
|
||||
background: white;
|
||||
}
|
||||
|
||||
|
||||
.active {
|
||||
background: #409EFF;
|
||||
color: white;
|
||||
}
|
||||
|
||||
|
||||
@media screen and (max-width: 767px) {
|
||||
.foot-left {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.foot-right {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.upload-file-card {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
#noscrollbar {
|
||||
padding-top: 20px;
|
||||
width: 100%;
|
||||
height: calc(100% - 60px);
|
||||
}
|
||||
|
||||
#upload-area {
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
#noscrollbar::-webkit-scrollbar {
|
||||
width: 0 !important
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,273 @@
|
||||
let uploadArea = document.getElementById("upload-box");
|
||||
let uploadFileCards = document.getElementById("upload-files");
|
||||
let selectQuality = document.getElementById("selectQuality");
|
||||
let selectLanguage = document.getElementById("selectLanguage");
|
||||
|
||||
let uploadMaxFileSize = 1024 * 1024 * 1024;
|
||||
let setting = "ebook";
|
||||
let action = "compress"
|
||||
let languages = ""
|
||||
|
||||
window.onload = function () {
|
||||
addUploadEventListener();
|
||||
addSettingBtnListener();
|
||||
};
|
||||
|
||||
function addSettingBtnListener() {
|
||||
let btns = document.getElementsByClassName("quality");
|
||||
for (let btn of btns) {
|
||||
btn.addEventListener("click", function () {
|
||||
for (let btn0 of document.getElementsByClassName("quality")) {
|
||||
btn0.className = "checkbox quality";
|
||||
}
|
||||
btn.className = "checkbox quality active";
|
||||
setting = btn.getAttribute("value");
|
||||
});
|
||||
}
|
||||
|
||||
btns = document.getElementsByClassName("menu");
|
||||
for (let btn of btns) {
|
||||
btn.addEventListener("click", function () {
|
||||
for (let btn0 of document.getElementsByClassName("menu")) {
|
||||
btn0.className = "checkbox menu";
|
||||
}
|
||||
btn.className = "checkbox menu active";
|
||||
action = btn.getAttribute("value");
|
||||
if (action === "compress") {
|
||||
selectQuality.style.display = "block";
|
||||
selectLanguage.style.display = "none";
|
||||
} else if (action === "ocr") {
|
||||
selectQuality.style.display = "none";
|
||||
selectLanguage.style.display = "block";
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
btns = document.getElementsByClassName("language");
|
||||
|
||||
for (let btn of btns) {
|
||||
btn.addEventListener("click",function(){
|
||||
|
||||
if(btn.className.indexOf("active") === -1){
|
||||
btn.className = "checkbox language active"
|
||||
}else{
|
||||
btn.className = "checkbox language"
|
||||
}
|
||||
languages = ""
|
||||
for(let btn0 of document.getElementsByClassName("language")){
|
||||
if(btn0.className.indexOf("active") !== -1){
|
||||
languages = languages + "," + btn0.getAttribute("value")
|
||||
}
|
||||
}
|
||||
languages = languages.substr(1)
|
||||
})
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
function addUploadEventListener() {
|
||||
uploadArea.addEventListener("click", () => {
|
||||
let fileInput = document.createElement("input");
|
||||
fileInput.setAttribute("type", "file");
|
||||
fileInput.setAttribute("style", "visibility:hidden");
|
||||
fileInput.setAttribute("multiple", "multiple");
|
||||
fileInput.setAttribute("accept", "application/pdf");
|
||||
fileInput.addEventListener("change", function () {
|
||||
for (let i = 0; i < this.files.length && i < 10; i++) {
|
||||
uploadFile(this.files[i]);
|
||||
}
|
||||
});
|
||||
fileInput.click();
|
||||
});
|
||||
}
|
||||
|
||||
function wellSize(num) {
|
||||
if (num <= 1024) {
|
||||
return num + "byte";
|
||||
}
|
||||
if (num <= 1024 * 1024) {
|
||||
return (num / 1024).toFixed(2) + "kb";
|
||||
}
|
||||
if (num <= 1024 * 1024 * 1024) {
|
||||
return (num / 1024 / 1024).toFixed(2) + "mb";
|
||||
}
|
||||
if (num <= 1024 * 1024 * 1024 * 1024) {
|
||||
return (num / 1024 / 1024 / 1024).toFixed(2) + "gb";
|
||||
}
|
||||
|
||||
return (num / 1024 / 1024 / 1024 / 1024).toFixed(2) + "tb";
|
||||
}
|
||||
|
||||
function addUploadCard(callback) {
|
||||
callback = callback || function () {};
|
||||
let addCard = () => {
|
||||
let cardHtml = `<div class="upload-file-thumb">
|
||||
<svg style="margin-top:10px" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="100" height="100"><path d="M136.533604 0.00026a49.119975 49.119975 0 0 0-35.839982 15.359992C91.307627 25.600247 85.33463 38.40024 85.33463 51.200234v921.599532a49.119975 49.119975 0 0 0 15.359992 35.839982 50.545974 50.545974 0 0 0 35.839982 15.359992h750.931619a49.119975 49.119975 0 0 0 35.839981-15.359992 50.546974 50.546974 0 0 0 15.359993-35.839982V290.134113L648.533344 0.00026z" fill="#FF5562"></path><path d="M938.666197 290.133113H699.733318a52.492973 52.492973 0 0 1-51.199974-51.199974V0.00026z" fill="#FFBBC0" ></path><path d="M730.266302 865.332821c-53.759973 0-101.599948-92.212953-127.146935-151.999923-42.666978-17.919991-89.599955-34.132983-134.826931-45.226977-40.10498 26.560987-107.518945 65.760967-159.624919 65.760966-32.426984 0-55.466972-16.212992-63.999968-44.372977-6.826997-23.039988-0.853-39.25298 5.972997-47.786976q20.47999-28.159986 84.479957-28.159986c34.132983 0 77.652961 5.972997 126.292936 17.919991a762.015613 762.015613 0 0 0 91.306954-75.092962c-12.799994-59.73297-26.452987-156.159921 8.532995-200.532898 17.066991-21.332989 43.519978-28.159986 75.092962-18.77299 34.986982 10.239995 47.786976 31.572984 51.999974 47.786976 14.506993 58.026971-51.999974 136.532931-97.332951 182.666907 10.239995 40.10698 23.039988 81.919958 39.25298 120.319939C695.33332 716.799896 772.080281 759.466874 780.666277 806.399851c3.412998 16.212992-1.706999 31.572984-14.506993 44.372977-11.092994 9.332995-23.039988 14.506993-35.839982 14.506993z m-79.359959-129.706935C683.333326 801.332853 714.000311 831.999838 730.266302 831.999838c2.559999 0 5.972997-0.853 11.092995-5.119998 5.972997-5.972997 5.972997-10.239995 5.119997-13.652993-3.412998-17.066991-30.666984-45.226977-95.572951-77.652961zM335.173503 647.732931c-41.812979 0-53.759973 10.239995-57.172971 14.506993-0.853 1.706999-4.266998 5.972997-0.852999 17.919991 2.559999 10.239995 9.332995 20.47999 31.572984 20.479989 27.306986 0 66.559966-15.359992 112.639942-42.666978-33.332983-6.826997-62.292968-10.239995-86.186956-10.239995z m168.959914-5.119997q41.577979 11.725994 81.919959 27.306986c-9.332995-24.746987-17.066991-50.346974-23.892988-75.092962-18.77299 16.212992-38.399981 32.426984-58.026971 47.786976z m105.866947-275.67986c-9.332995 0-16.212992 3.412998-22.186989 10.239994-17.919991 22.186989-19.62699 78.50696-5.972997 150.186924 51.999974-55.466972 80.212959-106.666946 73.332963-133.972932-0.853-4.266998-4.266998-16.212992-28.159986-23.039988a46.412976 46.412976 0 0 0-17.012991-3.413998z" fill="#FFFFFF"></path></svg>
|
||||
</div>
|
||||
<div class="file-links-box" style="display:none">
|
||||
<a class="download_button">点击下载</a>
|
||||
<span class="fileinfo"></span>
|
||||
</div>
|
||||
<div class="upload-error-cover" style="display:none">
|
||||
<svg class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="30" height="30"><path fill="red" d="M512 53.248c129.707008 3.412992 237.739008 48.299008 324.096 134.656S967.339008 382.292992 970.752 512c-3.412992 129.707008-48.299008 237.739008-134.656 324.096S641.707008 967.339008 512 970.752c-129.707008-3.412992-237.739008-48.299008-324.096-134.656S56.660992 641.707008 53.248 512c3.412992-129.707008 48.299008-237.739008 134.656-324.096S382.292992 56.660992 512 53.248z m0 403.456L405.504 350.208c-8.192-8.192-17.579008-12.288-28.16-12.288-10.580992 0-19.796992 3.924992-27.648 11.776-7.851008 7.851008-11.776 17.067008-11.776 27.648 0 10.580992 4.096 19.968 12.288 28.16l106.496 106.496-106.496 106.496c-8.192 8.192-12.288 17.579008-12.288 28.16 0 10.580992 3.924992 19.796992 11.776 27.648 7.851008 7.851008 17.067008 11.776 27.648 11.776 10.580992 0 19.968-4.096 28.16-12.288l106.496-106.496 106.496 106.496c10.923008 10.24 23.552 13.483008 37.888 9.728s23.380992-12.8 27.136-27.136c3.755008-14.336 0.512-26.964992-9.728-37.888L567.296 512l106.496-106.496c8.192-8.192 12.288-17.579008 12.288-28.16 0-10.580992-3.924992-19.796992-11.776-27.648-7.851008-7.851008-17.067008-11.776-27.648-11.776-10.580992 0-19.968 4.096-28.16 12.288L512 456.704z" ></path></svg>
|
||||
<div style="height:8px"></div>
|
||||
<span class="errorReason"></span>
|
||||
</div>
|
||||
<div class="upload-progress">
|
||||
<div class="progress-bar" style="width: 0%;"></div>
|
||||
</div>
|
||||
`;
|
||||
let card = document.createElement("div");
|
||||
card.className = "upload-file-card";
|
||||
card.innerHTML = cardHtml;
|
||||
uploadFileCards.prepend(card);
|
||||
let progressBar = card.getElementsByClassName("progress-bar")[0];
|
||||
let errReason = card.getElementsByClassName("errorReason")[0];
|
||||
let uploadErrorCover = card.getElementsByClassName("upload-error-cover")[0];
|
||||
let downloadButton = card.getElementsByClassName("download_button")[0];
|
||||
let fileinfo = card.getElementsByClassName("fileinfo")[0];
|
||||
let fileLinksBox = card.getElementsByClassName("file-links-box")[0];
|
||||
let setErrorInfo = function (errorText) {
|
||||
progressBar.style.background = "#F56C6C";
|
||||
errReason.innerText = errorText;
|
||||
uploadErrorCover.style.display = "block";
|
||||
};
|
||||
callback(
|
||||
(percent) => {
|
||||
progressBar.style.width = percent + "%";
|
||||
},
|
||||
(taskID) => {
|
||||
let timer = setInterval(() => {
|
||||
getTaskStatus(
|
||||
taskID,
|
||||
function (task) {
|
||||
if (task.status === 3 || task.status === 4) {
|
||||
clearInterval(timer);
|
||||
}
|
||||
if (task.status < 3) {
|
||||
// 下载进度
|
||||
progressBar.style.background = "#ffc940";
|
||||
progressBar.style.width = task.progress * 100 + "%";
|
||||
}
|
||||
|
||||
if (task.status === 3) {
|
||||
progressBar.style.background = "#ffc940";
|
||||
progressBar.style.width = task.progress * 100 + "%";
|
||||
downloadButton.setAttribute(
|
||||
"href",
|
||||
`/download?taskId=${taskID}`
|
||||
);
|
||||
|
||||
if(task.taskType === "compress"){
|
||||
fileinfo.innerHTML = `${task.name} 原大小:<b>${wellSize(
|
||||
task.size
|
||||
)}</b> 压缩后大小:<b>${wellSize(task.size2)}</b>`;
|
||||
fileLinksBox.style.display = "block";
|
||||
}else if(task.taskType === "ocr"){
|
||||
fileinfo.innerHTML = `${task.name} 转换完毕`;
|
||||
fileLinksBox.style.display = "block";
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
if (task.status === 4) {
|
||||
progressBar.style.background = "red";
|
||||
progressBar.style.width = task.progress * 100 + "%";
|
||||
setErrorInfo(`转换失败,原因是:${task.reason}`);
|
||||
}
|
||||
},
|
||||
function () {
|
||||
clearInterval(timer);
|
||||
setErrorInfo("转换失败");
|
||||
}
|
||||
);
|
||||
}, 500);
|
||||
},
|
||||
(reason) => {
|
||||
setErrorInfo(reason);
|
||||
}
|
||||
);
|
||||
};
|
||||
addCard();
|
||||
}
|
||||
|
||||
function getTaskStatus(taskID, callback, errCalback) {
|
||||
callback = callback || function () {};
|
||||
errCalback = errCalback || function () {};
|
||||
let request = new XMLHttpRequest();
|
||||
request.open("GET", `/api/task?taskId=${taskID}`);
|
||||
request.addEventListener("load", (e) => {
|
||||
let resp = JSON.parse(request.response);
|
||||
callback(resp);
|
||||
});
|
||||
|
||||
request.addEventListener("error", (e) => {
|
||||
errCalback("获取状态出错");
|
||||
});
|
||||
|
||||
request.send();
|
||||
}
|
||||
|
||||
function uploadFile(file) {
|
||||
addUploadCard((setProgress, uploadSuccess, uploadError) => {
|
||||
// 检查是否允许上传
|
||||
let size = file.size;
|
||||
if (size > uploadMaxFileSize) {
|
||||
uploadError("超过了上传文件最大限制");
|
||||
return;
|
||||
}
|
||||
ajaxUploadFile(file, setProgress, uploadSuccess, uploadError);
|
||||
});
|
||||
}
|
||||
|
||||
function ajaxUploadFile(file, setProgress, uploadSuccess, uploadError) {
|
||||
let formData = new FormData();
|
||||
formData.append("file", file);
|
||||
formData.append("setting", setting);
|
||||
formData.append("languages", languages);
|
||||
|
||||
let request = new XMLHttpRequest();
|
||||
request.open("POST", `/api/${action}`);
|
||||
request.upload.addEventListener("progress", (e) => {
|
||||
let percent_complete = (e.loaded / e.total) * 100;
|
||||
setProgress(percent_complete);
|
||||
});
|
||||
|
||||
request.addEventListener("load", (e) => {
|
||||
let resp = JSON.parse(request.response);
|
||||
if (request.status === 200) {
|
||||
uploadSuccess(resp.taskID);
|
||||
}
|
||||
});
|
||||
|
||||
request.addEventListener("error", (e) => {
|
||||
uploadError("上传出错");
|
||||
});
|
||||
|
||||
request.send(formData);
|
||||
}
|
||||
|
||||
function isMobile() {
|
||||
var userAgentInfo = navigator.userAgent;
|
||||
var mobileAgents = [
|
||||
"Android",
|
||||
"iPhone",
|
||||
"SymbianOS",
|
||||
"Windows Phone",
|
||||
"iPad",
|
||||
"iPod",
|
||||
];
|
||||
var mobile_flag = false;
|
||||
for (var v = 0; v < mobileAgents.length; v++) {
|
||||
if (userAgentInfo.indexOf(mobileAgents[v]) > 0) {
|
||||
mobile_flag = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
var screen_width = window.screen.width;
|
||||
var screen_height = window.screen.height;
|
||||
if (screen_width < 500 && screen_height < 800) {
|
||||
mobile_flag = true;
|
||||
}
|
||||
return mobile_flag;
|
||||
}
|
||||
@@ -0,0 +1,111 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"sync"
|
||||
)
|
||||
|
||||
type TaskType string
|
||||
|
||||
const (
|
||||
Compress TaskType = "compress"
|
||||
OCR TaskType = "ocr"
|
||||
)
|
||||
|
||||
type Task struct {
|
||||
ID string `json:"id"`
|
||||
TaskType TaskType `json:"taskType"`
|
||||
CreateTime int64 `json:"createTime"`
|
||||
Name string `json:"name"`
|
||||
OutName string `json:"outName"`
|
||||
Status HandleStatus `json:"status"`
|
||||
Progress float32 `json:"progress"`
|
||||
Reason string `json:"reason"`
|
||||
Size int64 `json:"size"`
|
||||
Size2 int64 `json:"size2"`
|
||||
}
|
||||
|
||||
type TaskService struct {
|
||||
taskList *sync.Map
|
||||
lock *sync.RWMutex
|
||||
}
|
||||
|
||||
func NewTaskService() *TaskService {
|
||||
taskList := sync.Map{}
|
||||
lock := sync.RWMutex{}
|
||||
return &TaskService{
|
||||
taskList: &taskList,
|
||||
lock: &lock,
|
||||
}
|
||||
}
|
||||
|
||||
func (t *TaskService) AddTask(task *Task) string {
|
||||
|
||||
t.lock.Lock()
|
||||
t.taskList.Store(task.ID, task)
|
||||
t.lock.Unlock()
|
||||
return task.ID
|
||||
}
|
||||
|
||||
func (t *TaskService) GetTask(id string) (*Task, error) {
|
||||
t.lock.RLock()
|
||||
task, ok := t.taskList.Load(id)
|
||||
t.lock.RUnlock()
|
||||
|
||||
if ok {
|
||||
return task.(*Task), nil
|
||||
}
|
||||
return nil, fmt.Errorf("unexist task [%s]", id)
|
||||
}
|
||||
|
||||
func (t *TaskService) GetTaskList() []*Task {
|
||||
tasks := []*Task{}
|
||||
t.lock.RLock()
|
||||
t.taskList.Range(func(key, value interface{}) bool {
|
||||
tasks = append(tasks, value.(*Task))
|
||||
return true
|
||||
})
|
||||
t.lock.RUnlock()
|
||||
|
||||
return tasks
|
||||
}
|
||||
|
||||
func (t *TaskService) UpdateTask(id string, status HandleStatus, progress float32, reason string) error {
|
||||
t.lock.RLock()
|
||||
taskInter, ok := t.taskList.Load(id)
|
||||
t.lock.RUnlock()
|
||||
|
||||
if ok {
|
||||
t.lock.Lock()
|
||||
task := taskInter.(*Task)
|
||||
task.Status = status
|
||||
task.Progress = progress
|
||||
task.Reason = reason
|
||||
t.taskList.Store(id, task)
|
||||
t.lock.Unlock()
|
||||
}
|
||||
return fmt.Errorf("unexist task [%s]", id)
|
||||
}
|
||||
|
||||
func (t *TaskService) UpdateTaskFileSize(id string, size2 int64) error {
|
||||
t.lock.RLock()
|
||||
taskInter, ok := t.taskList.Load(id)
|
||||
t.lock.RUnlock()
|
||||
|
||||
if ok {
|
||||
t.lock.Lock()
|
||||
task := taskInter.(*Task)
|
||||
task.Status = Success
|
||||
task.Progress = 1
|
||||
task.Size2 = size2
|
||||
t.taskList.Store(id, task)
|
||||
t.lock.Unlock()
|
||||
}
|
||||
return fmt.Errorf("unexist task [%s]", id)
|
||||
}
|
||||
|
||||
func (t *TaskService) DelTask(id string) {
|
||||
t.lock.Lock()
|
||||
t.taskList.Delete(id)
|
||||
t.lock.Unlock()
|
||||
}
|
||||
Reference in New Issue
Block a user