初始化
This commit is contained in:
3
.gitignore
vendored
3
.gitignore
vendored
@@ -1,3 +1,6 @@
|
||||
build/bin
|
||||
node_modules
|
||||
frontend/dist
|
||||
/.opencode/
|
||||
/.mimocode/
|
||||
/.qoder/
|
||||
|
||||
119
README.md
119
README.md
@@ -1,19 +1,114 @@
|
||||
# README
|
||||
# 年糕工具
|
||||
|
||||
## About
|
||||
一站式文件处理工具箱,支持 PDF、Word、Excel、图片的格式转换、编辑和处理。
|
||||
|
||||
This is the official Wails Vue template.
|
||||
## 功能特性
|
||||
|
||||
You can configure the project by editing `wails.json`. More information about the project settings can be found
|
||||
here: https://wails.io/docs/reference/project-config
|
||||
### PDF 工具
|
||||
- PDF 瘦身(压缩优化)
|
||||
- PDF 转图片
|
||||
- PDF 转 Word / Excel / HTML / Markdown
|
||||
- PDF 提取文本
|
||||
- PDF 合并
|
||||
- PDF 分割(按页码范围)
|
||||
- PDF 旋转
|
||||
- PDF 添加水印
|
||||
- 文本 / Markdown / HTML 转 PDF
|
||||
|
||||
## Live Development
|
||||
### 图片工具
|
||||
- 图片格式转换(JPEG / PNG / GIF / BMP / TIFF)
|
||||
- 图片压缩(预设:高质量 / 中等 / 小文件 / 极小)
|
||||
- 图片调整大小
|
||||
- 图片旋转
|
||||
- 图片裁剪
|
||||
- 灰度处理
|
||||
- 亮度 / 对比度 / 饱和度调节
|
||||
- 锐化 / 模糊 / 反色
|
||||
- 背景去除
|
||||
- 图片转 PDF
|
||||
|
||||
To run in live development mode, run `wails dev` in the project directory. This will run a Vite development
|
||||
server that will provide very fast hot reload of your frontend changes. If you want to develop in a browser
|
||||
and have access to your Go methods, there is also a dev server that runs on http://localhost:34115. Connect
|
||||
to this in your browser, and you can call your Go code from devtools.
|
||||
### 其他
|
||||
- Word 转 PDF
|
||||
- Excel 转 CSV / JSON
|
||||
- 运行日志(支持日期筛选)
|
||||
|
||||
## Building
|
||||
## 技术栈
|
||||
|
||||
To build a redistributable, production mode package, use `wails build`.
|
||||
- **后端**: Go + Wails v2
|
||||
- **前端**: Vue 3 + Element Plus + Vite
|
||||
- **PDF 处理**: pdfcpu(纯 Go)
|
||||
- **图片处理**: imaging
|
||||
- **文本渲染**: gopdf
|
||||
|
||||
## 开发环境
|
||||
|
||||
### 前置条件
|
||||
|
||||
- Go 1.25+
|
||||
- Node.js
|
||||
- Wails CLI: `go install github.com/wailsapp/wails/v2/cmd/wails@latest`
|
||||
|
||||
### 开发模式
|
||||
|
||||
```bash
|
||||
wails dev
|
||||
```
|
||||
|
||||
### 打包构建
|
||||
|
||||
```bash
|
||||
.\build.ps1
|
||||
```
|
||||
|
||||
构建产物位于 `build\bin\年糕工具.exe`(单文件,MuPDF DLL 已嵌入)。
|
||||
|
||||
### 手动构建
|
||||
|
||||
```powershell
|
||||
# 1. 生成绑定(nofitz 避免 DLL 依赖检查)
|
||||
wails build -tags nofitz
|
||||
|
||||
# 2. 复制 DLL 到构建目录(嵌入用)
|
||||
Copy-Item public\MuPDFLib.dll build\bin\libmupdf.dll
|
||||
|
||||
# 3. 最终构建(skipbindings 跳过重新生成)
|
||||
wails build -skipbindings
|
||||
|
||||
# 4. 重命名
|
||||
Copy-Item build\bin\xk.exe "build\bin\年糕工具.exe"
|
||||
```
|
||||
|
||||
## 项目结构
|
||||
|
||||
```
|
||||
├── main.go # 应用入口
|
||||
├── app.go # App 生命周期
|
||||
├── handlers.go # 文件处理 handler(IPC 接口)
|
||||
├── services/
|
||||
│ ├── pdf_service.go # PDF 处理(pdfcpu)
|
||||
│ ├── image_service.go # 图片处理(imaging)
|
||||
│ ├── word_service.go # Word 处理
|
||||
│ └── excel_service.go # Excel 处理
|
||||
├── frontend/src/
|
||||
│ ├── App.vue # 根组件
|
||||
│ ├── components/
|
||||
│ │ ├── ToolView.vue # 工具主页面
|
||||
│ │ ├── HomeView.vue # 首页
|
||||
│ │ ├── SettingsView.vue # 设置 + 日志查看
|
||||
│ │ ├── image/ # 图片工具组件
|
||||
│ │ └── pdf/ # PDF 工具组件
|
||||
│ └── router/index.js # 路由配置
|
||||
├── public/
|
||||
│ └── MuPDFLib.dll # MuPDF 库(嵌入到 exe)
|
||||
└── build.ps1 # 构建脚本
|
||||
```
|
||||
|
||||
## 日志
|
||||
|
||||
运行日志存储在 exe 同级 `logs/` 目录下,按日期分文件(`YYYY-MM-DD.log`)。
|
||||
|
||||
可在设置页面查看日志并按日期筛选。
|
||||
|
||||
## 许可证
|
||||
|
||||
MIT
|
||||
|
||||
10
app.go
10
app.go
@@ -3,6 +3,8 @@ package main
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
)
|
||||
|
||||
type App struct {
|
||||
@@ -20,3 +22,11 @@ func (a *App) startup(ctx context.Context) {
|
||||
func (a *App) Greet(name string) string {
|
||||
return fmt.Sprintf("Hello %s, It's show time!", name)
|
||||
}
|
||||
|
||||
func getExeDir() string {
|
||||
exe, err := os.Executable()
|
||||
if err != nil {
|
||||
return "."
|
||||
}
|
||||
return filepath.Dir(exe)
|
||||
}
|
||||
|
||||
28
build.ps1
Normal file
28
build.ps1
Normal file
@@ -0,0 +1,28 @@
|
||||
# 年糕工具 Build Script
|
||||
# Usage: .\build.ps1
|
||||
|
||||
$ErrorActionPreference = "Stop"
|
||||
$buildDir = "build\bin"
|
||||
|
||||
Write-Host ""
|
||||
Write-Host "=== 年糕工具 Build ===" -ForegroundColor Cyan
|
||||
Write-Host ""
|
||||
|
||||
# Build with nofitz (go-fitz excluded, DLL embedded)
|
||||
Write-Host "Building..." -ForegroundColor Yellow
|
||||
wails build -tags nofitz
|
||||
if ($LASTEXITCODE -ne 0) { Write-Host "Build failed!" -ForegroundColor Red; exit 1 }
|
||||
|
||||
# Rename using cmd to avoid encoding issues
|
||||
cmd /c "copy /Y `"$buildDir\xk.exe`" `"$buildDir\年糕工具.exe`""
|
||||
cmd /c "del /f /q `"$buildDir\xk.exe`""
|
||||
cmd /c "del /f /q `"$buildDir\xk-dev.exe`""
|
||||
|
||||
# Remove standalone DLL (it's embedded now)
|
||||
cmd /c "del /f /q `"$buildDir\libmupdf.dll`""
|
||||
cmd /c "del /f /q `"$buildDir\MuPDFLib.dll`""
|
||||
|
||||
Write-Host ""
|
||||
Write-Host "=== Build Complete ===" -ForegroundColor Green
|
||||
Write-Host " $buildDir\年糕工具.exe (single file, DLL embedded)" -ForegroundColor Gray
|
||||
Write-Host ""
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 130 KiB After Width: | Height: | Size: 992 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 20 KiB After Width: | Height: | Size: 817 B |
@@ -263,7 +263,6 @@ function closeWindow() {
|
||||
background: var(--bg-primary);
|
||||
}
|
||||
|
||||
/* Page Transition */
|
||||
.page-fade-enter-active,
|
||||
.page-fade-leave-active {
|
||||
transition: opacity 0.2s ease, transform 0.2s ease;
|
||||
|
||||
@@ -101,7 +101,7 @@ function formatTime(ts) {
|
||||
<div class="home-header animate-slide-up">
|
||||
<div class="home-title">
|
||||
<el-icon :size="28"><SetUp /></el-icon>
|
||||
<span>XK 文件工具箱</span>
|
||||
<span>年糕工具</span>
|
||||
</div>
|
||||
<el-button text @click="router.push('/settings')">
|
||||
<el-icon><Setting /></el-icon>
|
||||
|
||||
@@ -1,17 +1,42 @@
|
||||
<script setup>
|
||||
import { ref, onMounted } from 'vue'
|
||||
import { ref, onMounted, watch } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { ElMessage } from 'element-plus'
|
||||
|
||||
const router = useRouter()
|
||||
const config = ref({ defaultOutputDir: '' })
|
||||
const logDates = ref([])
|
||||
const selectedDate = ref('')
|
||||
const logContent = ref('')
|
||||
|
||||
onMounted(async () => {
|
||||
try {
|
||||
config.value = await window.go.main.FileHandler.GetConfig()
|
||||
} catch (e) {}
|
||||
await loadLogDates()
|
||||
})
|
||||
|
||||
async function loadLogDates() {
|
||||
try {
|
||||
logDates.value = await window.go.main.FileHandler.GetLogDates()
|
||||
if (logDates.value.length > 0 && !selectedDate.value) {
|
||||
selectedDate.value = logDates.value[logDates.value.length - 1]
|
||||
await loadLogs()
|
||||
}
|
||||
} catch (e) {}
|
||||
}
|
||||
|
||||
async function loadLogs() {
|
||||
if (!selectedDate.value) { logContent.value = ''; return }
|
||||
try {
|
||||
logContent.value = await window.go.main.FileHandler.GetLogs(selectedDate.value)
|
||||
} catch (e) {
|
||||
logContent.value = ''
|
||||
}
|
||||
}
|
||||
|
||||
watch(selectedDate, () => loadLogs())
|
||||
|
||||
async function selectDir() {
|
||||
try {
|
||||
const path = await window.go.main.FileHandler.OpenFileDialog('选择默认输出目录', ['*'])
|
||||
@@ -83,7 +108,7 @@ function resetConfig() {
|
||||
<div class="about-info">
|
||||
<div class="about-row">
|
||||
<span class="about-label">应用名称</span>
|
||||
<span class="about-value">XK 文件工具箱</span>
|
||||
<span class="about-value">年糕工具</span>
|
||||
</div>
|
||||
<div class="about-row">
|
||||
<span class="about-label">版本</span>
|
||||
@@ -95,6 +120,24 @@ function resetConfig() {
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="settings-card">
|
||||
<div class="card-title">运行日志</div>
|
||||
<div class="card-desc">查看应用运行日志,用于排查问题</div>
|
||||
|
||||
<div class="form-group">
|
||||
<div class="form-label">选择日期</div>
|
||||
<div class="tag-group">
|
||||
<div v-for="date in logDates" :key="date" class="tag-option" :class="{ active: selectedDate === date }" @click="selectedDate = date">{{ date }}</div>
|
||||
<div v-if="logDates.length === 0" class="no-logs">暂无日志</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="log-viewer" v-if="logContent">
|
||||
<pre class="log-content">{{ logContent }}</pre>
|
||||
</div>
|
||||
<div v-else class="no-logs">选择日期查看日志</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -224,4 +267,37 @@ function resetConfig() {
|
||||
font-size: 13px;
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.tag-group { display: flex; gap: 6px; flex-wrap: wrap; margin-bottom: 12px; }
|
||||
.tag-option {
|
||||
display: flex; align-items: center; gap: 4px; padding: 7px 14px;
|
||||
border-radius: var(--radius-sm); font-size: 13px; font-weight: 500;
|
||||
cursor: pointer; background: var(--bg-surface); color: var(--text-secondary);
|
||||
border: 1px solid var(--glass-border); transition: all var(--transition-normal); user-select: none;
|
||||
}
|
||||
.tag-option:hover { color: var(--text-primary); border-color: rgba(255, 255, 255, 0.15); background: rgba(255, 255, 255, 0.05); }
|
||||
.tag-option.active { background: var(--accent-primary); color: #ffffff; border-color: var(--accent-primary); }
|
||||
|
||||
.log-viewer {
|
||||
background: #1a1a25;
|
||||
border: 1px solid var(--glass-border);
|
||||
border-radius: var(--radius-md);
|
||||
padding: 16px;
|
||||
max-height: 400px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
.log-content {
|
||||
font-family: 'Consolas', 'Monaco', monospace;
|
||||
font-size: 12px;
|
||||
color: var(--text-secondary);
|
||||
line-height: 1.6;
|
||||
white-space: pre-wrap;
|
||||
word-break: break-all;
|
||||
margin: 0;
|
||||
}
|
||||
.no-logs {
|
||||
font-size: 13px;
|
||||
color: var(--text-muted);
|
||||
padding: 12px 0;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -14,6 +14,16 @@ import ImageBlurTool from './image/ImageBlurTool.vue'
|
||||
import ImageInvertTool from './image/ImageInvertTool.vue'
|
||||
import ImageConvertTool from './image/ImageConvertTool.vue'
|
||||
import ImageRemoveBgTool from './image/ImageRemoveBgTool.vue'
|
||||
import ImageToPdfTool from './image/ImageToPdfTool.vue'
|
||||
|
||||
import PdfCompressTool from './pdf/PdfCompressTool.vue'
|
||||
import PdfToImageTool from './pdf/PdfToImageTool.vue'
|
||||
import PdfExtractTextTool from './pdf/PdfExtractTextTool.vue'
|
||||
import PdfSplitTool from './pdf/PdfSplitTool.vue'
|
||||
import PdfRotateTool from './pdf/PdfRotateTool.vue'
|
||||
import PdfWatermarkTool from './pdf/PdfWatermarkTool.vue'
|
||||
import PdfMergeTool from './pdf/PdfMergeTool.vue'
|
||||
import PdfFromTextTool from './pdf/PdfFromTextTool.vue'
|
||||
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
@@ -22,6 +32,7 @@ const category = computed(() => route.params.category || 'pdf')
|
||||
const action = ref(route.params.action || '')
|
||||
|
||||
const filePath = ref('')
|
||||
const fileLoading = ref(false)
|
||||
const filePreview = ref('')
|
||||
const imageInfo = ref(null)
|
||||
const processing = ref(false)
|
||||
@@ -48,10 +59,18 @@ const categoryConfig = {
|
||||
pdf: {
|
||||
name: 'PDF 工具',
|
||||
actions: [
|
||||
{ id: 'compress', name: '压缩PDF', icon: 'FolderChecked', color: '#f38ba8' },
|
||||
{ id: 'split', name: '分割PDF', icon: 'Document', color: '#f38ba8' },
|
||||
{ id: 'compress', name: 'PDF瘦身', icon: 'FolderChecked', color: '#f38ba8' },
|
||||
{ id: 'toImage', name: '转图片', icon: 'Picture', color: '#f38ba8' },
|
||||
{ id: 'toWord', name: '转Word', icon: 'Document', color: '#f38ba8' },
|
||||
{ id: 'toExcel', name: '转Excel', icon: 'Grid', color: '#f38ba8' },
|
||||
{ id: 'toHtml', name: '转HTML', icon: 'Monitor', color: '#f38ba8' },
|
||||
{ id: 'toMarkdown', name: '转Markdown', icon: 'Notebook', color: '#f38ba8' },
|
||||
{ id: 'toText', name: '提取文本', icon: 'Memo', color: '#f38ba8' },
|
||||
{ id: 'merge', name: '合并PDF', icon: 'DocumentCopy', color: '#f38ba8' },
|
||||
{ id: 'split', name: '分割PDF', icon: 'Scissor', color: '#f38ba8' },
|
||||
{ id: 'rotate', name: '旋转PDF', icon: 'RefreshRight', color: '#f38ba8' },
|
||||
{ id: 'watermark', name: '添加水印', icon: 'EditPen', color: '#f38ba8' },
|
||||
{ id: 'fromText', name: '文本转PDF', icon: 'Upload', color: '#f38ba8' },
|
||||
],
|
||||
fileFilters: ['*.pdf'],
|
||||
},
|
||||
@@ -84,6 +103,7 @@ const categoryConfig = {
|
||||
{ id: 'blur', name: '模糊', icon: 'View', color: '#f9e2af' },
|
||||
{ id: 'invert', name: '反色', icon: 'RefreshLeft', color: '#f9e2af' },
|
||||
{ id: 'removebg', name: '背景去除', icon: 'MagicStick', color: '#f9e2af' },
|
||||
{ id: 'toPdf', name: '转PDF', icon: 'Document', color: '#f9e2af' },
|
||||
],
|
||||
fileFilters: ['*.jpg;*.jpeg;*.png;*.gif;*.bmp;*.tiff;*.webp;*.ico'],
|
||||
},
|
||||
@@ -151,15 +171,25 @@ function setAction(id) {
|
||||
|
||||
async function selectFile() {
|
||||
try {
|
||||
const path = await window.go.main.FileHandler.OpenFileDialog('选择文件', currentCat.value.fileFilters)
|
||||
let filters = currentCat.value.fileFilters
|
||||
if (category.value === 'pdf' && action.value === 'fromText') {
|
||||
filters = ['*.txt;*.md;*.html']
|
||||
}
|
||||
const path = await window.go.main.FileHandler.OpenFileDialog('选择文件', filters)
|
||||
if (path) {
|
||||
filePath.value = path
|
||||
resetState()
|
||||
filePath.value = path
|
||||
outputPath.value = ''
|
||||
await loadFilePreview()
|
||||
fileLoading.value = true
|
||||
try {
|
||||
await loadFilePreview()
|
||||
} finally {
|
||||
fileLoading.value = false
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
ElMessage.error('选择文件失败: ' + e.message)
|
||||
try { await window.go.main.FileHandler.LogError('[SelectFile] ' + e.message) } catch (err) {}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -203,13 +233,23 @@ async function selectOutputPath() {
|
||||
}
|
||||
|
||||
function getOutputFilters() {
|
||||
if (category.value === 'pdf') return ['*.pdf']
|
||||
if (category.value === 'pdf') {
|
||||
if (action.value === 'toImage') return ['*.png', '*.jpg;*.jpeg']
|
||||
if (action.value === 'toWord') return ['*.txt']
|
||||
if (action.value === 'toExcel') return ['*.csv']
|
||||
if (action.value === 'toHtml') return ['*.html']
|
||||
if (action.value === 'toMarkdown') return ['*.md']
|
||||
if (action.value === 'toText') return ['*.txt']
|
||||
if (action.value === 'fromText') return ['*.pdf']
|
||||
return ['*.pdf']
|
||||
}
|
||||
if (category.value === 'word') return ['*.pdf']
|
||||
if (category.value === 'excel') {
|
||||
return action.value === 'json' ? ['*.json'] : ['*.csv']
|
||||
}
|
||||
if (category.value === 'image') {
|
||||
if (action.value === 'convert') return ['*.jpg;*.jpeg', '*.png', '*.gif', '*.bmp', '*.tiff', '*.ico']
|
||||
if (action.value === 'toPdf') return ['*.pdf']
|
||||
return ['*.jpg;*.jpeg', '*.png']
|
||||
}
|
||||
return ['*']
|
||||
@@ -223,11 +263,20 @@ function getOutputDefaultName() {
|
||||
}
|
||||
|
||||
function getOutputExt() {
|
||||
if (category.value === 'pdf') return '.pdf'
|
||||
if (category.value === 'pdf') {
|
||||
if (action.value === 'toImage') return '.png'
|
||||
if (action.value === 'toWord') return '.txt'
|
||||
if (action.value === 'toExcel') return '.csv'
|
||||
if (action.value === 'toHtml') return '.html'
|
||||
if (action.value === 'toMarkdown') return '.md'
|
||||
if (action.value === 'toText') return '.txt'
|
||||
return '.pdf'
|
||||
}
|
||||
if (category.value === 'word') return '.pdf'
|
||||
if (category.value === 'excel') return action.value === 'json' ? '.json' : '.csv'
|
||||
if (category.value === 'image') {
|
||||
if (action.value === 'convert') return '.' + outputFormat.value
|
||||
if (action.value === 'toPdf') return '.pdf'
|
||||
return '.png'
|
||||
}
|
||||
return ''
|
||||
@@ -289,11 +338,13 @@ async function processFile() {
|
||||
})
|
||||
|
||||
ElMessage.success('处理完成')
|
||||
try { await window.go.main.FileHandler.LogInfo('[Process] ' + req.format + ' -> ' + result.path) } catch (err) {}
|
||||
} else {
|
||||
ElMessage.error(result.message)
|
||||
}
|
||||
} catch (e) {
|
||||
ElMessage.error('处理失败: ' + e.message)
|
||||
try { await window.go.main.FileHandler.LogError('[ProcessFile] ' + e.message) } catch (err) {}
|
||||
} finally {
|
||||
processing.value = false
|
||||
}
|
||||
@@ -343,11 +394,13 @@ async function handleImageProcess(req) {
|
||||
})
|
||||
|
||||
ElMessage.success('处理完成')
|
||||
try { await window.go.main.FileHandler.LogInfo('[Process] ' + req.format + ' -> ' + result.path) } catch (err) {}
|
||||
} else {
|
||||
ElMessage.error(result.message)
|
||||
}
|
||||
} catch (e) {
|
||||
ElMessage.error('处理失败: ' + e.message)
|
||||
try { await window.go.main.FileHandler.LogError('[ProcessFile] ' + e.message) } catch (err) {}
|
||||
} finally {
|
||||
processing.value = false
|
||||
}
|
||||
@@ -444,6 +497,14 @@ const imageToolProps = computed(() => ({
|
||||
config: config.value,
|
||||
}))
|
||||
|
||||
const pdfToolProps = computed(() => ({
|
||||
filePath: filePath.value,
|
||||
processing: processing.value,
|
||||
resultInfo: resultInfo.value,
|
||||
outputPath: outputPath.value,
|
||||
config: config.value,
|
||||
}))
|
||||
|
||||
const isImageWorkspaceTool = computed(() => {
|
||||
return category.value === 'image' && ['compress', 'resize', 'rotate', 'crop', 'grayscale', 'brightness', 'sharpen', 'blur', 'invert'].includes(action.value) && !!filePath.value
|
||||
})
|
||||
@@ -482,53 +543,81 @@ const isImageRemoveBgTool = computed(() => category.value === 'image' && action.
|
||||
</div>
|
||||
|
||||
<div class="tool-body">
|
||||
<ImageCompressTool v-if="action === 'compress' && category === 'image'" v-bind="imageToolProps" @process="handleImageProcess" @selectFile="selectFile" />
|
||||
<ImageResizeTool v-else-if="action === 'resize' && category === 'image'" v-bind="imageToolProps" @process="handleImageProcess" @selectFile="selectFile" />
|
||||
<ImageRotateTool v-else-if="action === 'rotate' && category === 'image'" v-bind="imageToolProps" @process="handleImageProcess" @selectFile="selectFile" />
|
||||
<ImageCropTool v-else-if="action === 'crop' && category === 'image'" v-bind="imageToolProps" @process="handleImageProcess" @selectFile="selectFile" />
|
||||
<ImageGrayscaleTool v-else-if="action === 'grayscale' && category === 'image'" v-bind="imageToolProps" @process="handleImageProcess" @selectFile="selectFile" />
|
||||
<ImageBrightnessTool v-else-if="action === 'brightness' && category === 'image'" v-bind="imageToolProps" @process="handleImageProcess" @selectFile="selectFile" />
|
||||
<ImageSharpenTool v-else-if="action === 'sharpen' && category === 'image'" v-bind="imageToolProps" @process="handleImageProcess" @selectFile="selectFile" />
|
||||
<ImageBlurTool v-else-if="action === 'blur' && category === 'image'" v-bind="imageToolProps" @process="handleImageProcess" @selectFile="selectFile" />
|
||||
<ImageInvertTool v-else-if="action === 'invert' && category === 'image'" v-bind="imageToolProps" @process="handleImageProcess" @selectFile="selectFile" />
|
||||
<ImageConvertTool v-else-if="action === 'convert' && category === 'image'" v-bind="imageToolProps" @process="handleImageProcess" @selectFile="selectFile" />
|
||||
<ImageRemoveBgTool v-else-if="action === 'removebg' && category === 'image'" v-bind="imageToolProps" @process="handleImageProcess" @selectFile="selectFile" />
|
||||
|
||||
<div v-else class="tool-main">
|
||||
<div class="file-drop-zone" @click="selectFile" :class="{ 'has-file': filePath }">
|
||||
<template v-if="!filePath">
|
||||
<template v-if="category === 'image'">
|
||||
<ImageCompressTool v-if="action === 'compress'" v-bind="imageToolProps" @process="handleImageProcess" @selectFile="selectFile" />
|
||||
<ImageResizeTool v-else-if="action === 'resize'" v-bind="imageToolProps" @process="handleImageProcess" @selectFile="selectFile" />
|
||||
<ImageRotateTool v-else-if="action === 'rotate'" v-bind="imageToolProps" @process="handleImageProcess" @selectFile="selectFile" />
|
||||
<ImageCropTool v-else-if="action === 'crop'" v-bind="imageToolProps" @process="handleImageProcess" @selectFile="selectFile" />
|
||||
<ImageGrayscaleTool v-else-if="action === 'grayscale'" v-bind="imageToolProps" @process="handleImageProcess" @selectFile="selectFile" />
|
||||
<ImageBrightnessTool v-else-if="action === 'brightness'" v-bind="imageToolProps" @process="handleImageProcess" @selectFile="selectFile" />
|
||||
<ImageSharpenTool v-else-if="action === 'sharpen'" v-bind="imageToolProps" @process="handleImageProcess" @selectFile="selectFile" />
|
||||
<ImageBlurTool v-else-if="action === 'blur'" v-bind="imageToolProps" @process="handleImageProcess" @selectFile="selectFile" />
|
||||
<ImageInvertTool v-else-if="action === 'invert'" v-bind="imageToolProps" @process="handleImageProcess" @selectFile="selectFile" />
|
||||
<ImageConvertTool v-else-if="action === 'convert'" v-bind="imageToolProps" @process="handleImageProcess" @selectFile="selectFile" />
|
||||
<ImageRemoveBgTool v-else-if="action === 'removebg'" v-bind="imageToolProps" @process="handleImageProcess" @selectFile="selectFile" />
|
||||
<ImageToPdfTool v-else-if="action === 'toPdf'" v-bind="imageToolProps" @process="handleImageProcess" @selectFile="selectFile" />
|
||||
<div v-else class="tool-main">
|
||||
<div class="file-drop-zone" @click="selectFile">
|
||||
<el-icon :size="40" class="drop-icon"><UploadFilled /></el-icon>
|
||||
<div class="drop-text">点击选择文件</div>
|
||||
<div class="drop-hint">支持拖拽文件到此处</div>
|
||||
</template>
|
||||
<template v-else>
|
||||
<div class="file-info">
|
||||
<el-icon :size="28" :style="{ color: currentAction?.color }">
|
||||
<component :is="currentAction?.icon || 'Document'" />
|
||||
</el-icon>
|
||||
<div class="file-details">
|
||||
<div class="file-name">{{ getFileName() }}</div>
|
||||
<div class="file-path">{{ filePath }}</div>
|
||||
</div>
|
||||
<el-button type="danger" text @click.stop="filePath = ''; resetState()">
|
||||
<el-icon><Delete /></el-icon>
|
||||
</el-button>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
|
||||
<div class="params-section">
|
||||
<div class="param-group" v-if="showOutputFormat">
|
||||
<div class="param-label">目标格式</div>
|
||||
<div class="tag-group">
|
||||
<div class="tag-option" :class="{ active: outputFormat === 'jpeg' }" @click="outputFormat = 'jpeg'">JPEG</div>
|
||||
<div class="tag-option" :class="{ active: outputFormat === 'png' }" @click="outputFormat = 'png'">PNG</div>
|
||||
<div class="tag-option" :class="{ active: outputFormat === 'gif' }" @click="outputFormat = 'gif'">GIF</div>
|
||||
<div class="tag-option" :class="{ active: outputFormat === 'bmp' }" @click="outputFormat = 'bmp'">BMP</div>
|
||||
<div class="tag-option" :class="{ active: outputFormat === 'tiff' }" @click="outputFormat = 'tiff'">TIFF</div>
|
||||
<div class="tag-option" :class="{ active: outputFormat === 'ico' }" @click="outputFormat = 'ico'">ICO</div>
|
||||
</div>
|
||||
<div class="drop-text">选择图片工具</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template v-else-if="category === 'pdf'">
|
||||
<PdfCompressTool v-if="action === 'compress'" v-bind="pdfToolProps" @process="handleImageProcess" @selectFile="selectFile" />
|
||||
<PdfToImageTool v-else-if="action === 'toImage'" v-bind="pdfToolProps" @process="handleImageProcess" @selectFile="selectFile" />
|
||||
<PdfExtractTextTool v-else-if="action === 'toText'" v-bind="pdfToolProps" @process="handleImageProcess" @selectFile="selectFile" />
|
||||
<PdfSplitTool v-else-if="action === 'split'" v-bind="pdfToolProps" @process="handleImageProcess" @selectFile="selectFile" />
|
||||
<PdfRotateTool v-else-if="action === 'rotate'" v-bind="pdfToolProps" @process="handleImageProcess" @selectFile="selectFile" />
|
||||
<PdfWatermarkTool v-else-if="action === 'watermark'" v-bind="pdfToolProps" @process="handleImageProcess" @selectFile="selectFile" />
|
||||
<PdfMergeTool v-else-if="action === 'merge'" v-bind="pdfToolProps" @process="handleImageProcess" @selectFile="selectFile" />
|
||||
<PdfFromTextTool v-else-if="action === 'fromText'" v-bind="pdfToolProps" @process="handleImageProcess" @selectFile="selectFile" />
|
||||
<PdfExtractTextTool v-else-if="['toWord', 'toExcel', 'toHtml', 'toMarkdown'].includes(action)" v-bind="pdfToolProps" @process="handleImageProcess" @selectFile="selectFile" />
|
||||
<div v-else class="tool-main">
|
||||
<div class="file-drop-zone" @click="selectFile">
|
||||
<el-icon :size="40" class="drop-icon"><UploadFilled /></el-icon>
|
||||
<div class="drop-text">选择 PDF 工具</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template v-else>
|
||||
<div class="tool-main">
|
||||
<div class="file-drop-zone" @click="selectFile" :class="{ 'has-file': filePath }">
|
||||
<template v-if="!filePath">
|
||||
<el-icon :size="40" class="drop-icon"><UploadFilled /></el-icon>
|
||||
<div class="drop-text">点击选择文件</div>
|
||||
<div class="drop-hint">支持拖拽文件到此处</div>
|
||||
</template>
|
||||
<template v-else>
|
||||
<div class="file-info">
|
||||
<el-icon :size="28" :style="{ color: currentAction?.color }">
|
||||
<component :is="currentAction?.icon || 'Document'" />
|
||||
</el-icon>
|
||||
<div class="file-details">
|
||||
<div class="file-name">{{ getFileName() }}</div>
|
||||
<div class="file-path">{{ filePath }}</div>
|
||||
</div>
|
||||
<el-button type="danger" text @click.stop="filePath = ''; resetState()">
|
||||
<el-icon><Delete /></el-icon>
|
||||
</el-button>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
|
||||
<div class="params-section">
|
||||
<div class="param-group" v-if="showOutputFormat">
|
||||
<div class="param-label">目标格式</div>
|
||||
<div class="tag-group">
|
||||
<div class="tag-option" :class="{ active: outputFormat === 'jpeg' }" @click="outputFormat = 'jpeg'">JPEG</div>
|
||||
<div class="tag-option" :class="{ active: outputFormat === 'png' }" @click="outputFormat = 'png'">PNG</div>
|
||||
<div class="tag-option" :class="{ active: outputFormat === 'gif' }" @click="outputFormat = 'gif'">GIF</div>
|
||||
<div class="tag-option" :class="{ active: outputFormat === 'bmp' }" @click="outputFormat = 'bmp'">BMP</div>
|
||||
<div class="tag-option" :class="{ active: outputFormat === 'tiff' }" @click="outputFormat = 'tiff'">TIFF</div>
|
||||
<div class="tag-option" :class="{ active: outputFormat === 'ico' }" @click="outputFormat = 'ico'">ICO</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="param-group" v-if="showQuality && category === 'pdf'">
|
||||
<div class="param-label">质量</div>
|
||||
@@ -569,8 +658,10 @@ const isImageRemoveBgTool = computed(() => category.value === 'image' && action.
|
||||
{{ processing ? '处理中...' : '开始处理' }}
|
||||
</el-button>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
|
||||
<transition name="fade-slide">
|
||||
<transition name="fade-slide">
|
||||
<div class="result-panel" v-if="resultInfo">
|
||||
<div class="result-header">
|
||||
<el-icon :size="20" style="color: #3fb950" class="success-icon"><SuccessFilled /></el-icon>
|
||||
@@ -634,7 +725,6 @@ const isImageRemoveBgTool = computed(() => category.value === 'image' && action.
|
||||
</div>
|
||||
</div>
|
||||
</transition>
|
||||
</div>
|
||||
|
||||
<transition name="fade">
|
||||
<div class="preview-overlay" v-if="showPreview" @click.self="closePreview">
|
||||
@@ -688,6 +778,15 @@ const isImageRemoveBgTool = computed(() => category.value === 'image' && action.
|
||||
</div>
|
||||
</div>
|
||||
</transition>
|
||||
|
||||
<transition name="fade">
|
||||
<div class="loading-overlay" v-if="fileLoading">
|
||||
<div class="loading-card">
|
||||
<div class="spinner"></div>
|
||||
<div class="loading-text">正在加载文件...</div>
|
||||
</div>
|
||||
</div>
|
||||
</transition>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
||||
@@ -22,12 +22,19 @@ const aspectLocked = ref(false)
|
||||
const customWidth = ref(800)
|
||||
const customHeight = ref(600)
|
||||
|
||||
watch(() => props.originalWidth, (w) => {
|
||||
if (w) customWidth.value = w
|
||||
})
|
||||
watch(() => props.originalHeight, (h) => {
|
||||
if (h) customHeight.value = h
|
||||
})
|
||||
const presets = [
|
||||
{ name: '高质量', quality: 90, desc: '几乎无损' },
|
||||
{ name: '中等', quality: 75, desc: '推荐' },
|
||||
{ name: '小文件', quality: 50, desc: '体积优先' },
|
||||
{ name: '极小', quality: 30, desc: '极限压缩' },
|
||||
]
|
||||
|
||||
function applyPreset(q) {
|
||||
qualityInt.value = q
|
||||
}
|
||||
|
||||
watch(() => props.originalWidth, (w) => { if (w) customWidth.value = w })
|
||||
watch(() => props.originalHeight, (h) => { if (h) customHeight.value = h })
|
||||
|
||||
function buildRequest() {
|
||||
return {
|
||||
@@ -41,9 +48,7 @@ function buildRequest() {
|
||||
}
|
||||
}
|
||||
|
||||
function onProcess() {
|
||||
emit('process', buildRequest())
|
||||
}
|
||||
function onProcess() { emit('process', buildRequest()) }
|
||||
|
||||
function formatSize(bytes) {
|
||||
if (!bytes) return '0 B'
|
||||
@@ -58,24 +63,15 @@ function getFileName() {
|
||||
return props.filePath.split(/[/\\]/).pop()
|
||||
}
|
||||
|
||||
function onDragOver(e) {
|
||||
e.preventDefault()
|
||||
e.currentTarget.classList.add('dragover')
|
||||
}
|
||||
|
||||
function onDragLeave(e) {
|
||||
e.currentTarget.classList.remove('dragover')
|
||||
}
|
||||
|
||||
function onDragOver(e) { e.preventDefault(); e.currentTarget.classList.add('dragover') }
|
||||
function onDragLeave(e) { e.currentTarget.classList.remove('dragover') }
|
||||
function onDrop(e) {
|
||||
e.preventDefault()
|
||||
e.currentTarget.classList.remove('dragover')
|
||||
e.preventDefault(); e.currentTarget.classList.remove('dragover')
|
||||
const files = e.dataTransfer?.files
|
||||
if (files?.length) {
|
||||
const ext = files[0].name.split('.').pop().toLowerCase()
|
||||
if (!['jpg', 'jpeg', 'png', 'gif', 'bmp', 'tiff', 'tif', 'webp', 'ico'].includes(ext)) {
|
||||
ElMessage.warning('请选择图片文件')
|
||||
return
|
||||
ElMessage.warning('请选择图片文件'); return
|
||||
}
|
||||
emit('selectFile', files[0].path)
|
||||
}
|
||||
@@ -99,15 +95,22 @@ function onDrop(e) {
|
||||
<div class="image-info-bar">
|
||||
<span class="image-dims">{{ originalWidth }} × {{ originalHeight }} px</span>
|
||||
<span class="image-filename">{{ getFileName() }}</span>
|
||||
<el-button type="danger" text size="small" @click="$emit('selectFile')">
|
||||
<el-icon><Delete /></el-icon>
|
||||
</el-button>
|
||||
<el-button type="danger" text size="small" @click="$emit('selectFile')"><el-icon><Delete /></el-icon></el-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="image-panel">
|
||||
<div class="panel-section">
|
||||
<div class="panel-title">压缩设置</div>
|
||||
<div class="param-group">
|
||||
<div class="param-label">快速预设</div>
|
||||
<div class="preset-group">
|
||||
<button v-for="p in presets" :key="p.quality" class="preset-btn" :class="{ active: qualityInt === p.quality }" @click="applyPreset(p.quality)">
|
||||
<span class="preset-name">{{ p.name }}</span>
|
||||
<span class="preset-desc">{{ p.desc }}</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="param-group">
|
||||
<div class="param-label">输出格式</div>
|
||||
<div class="tag-group">
|
||||
@@ -135,9 +138,6 @@ function onDrop(e) {
|
||||
<span class="size-unit">px</span>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="resultInfo && resultInfo.isImageOp" class="estimated-size">
|
||||
<span class="estimated-label">处理后大小:</span> {{ resultInfo.size }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="panel-section">
|
||||
@@ -149,126 +149,50 @@ function onDrop(e) {
|
||||
|
||||
<el-button class="process-btn" type="primary" size="large" :loading="processing" :disabled="!filePath" @click="onProcess">
|
||||
<el-icon v-if="!processing"><Check /></el-icon>
|
||||
{{ processing ? '处理中...' : '开始处理' }}
|
||||
{{ processing ? '处理中...' : '开始压缩' }}
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.file-drop-zone {
|
||||
border: 2px dashed var(--glass-border);
|
||||
border-radius: var(--radius-lg);
|
||||
padding: 80px 40px;
|
||||
text-align: center;
|
||||
cursor: pointer;
|
||||
transition: all var(--transition-normal);
|
||||
background: var(--glass-bg);
|
||||
backdrop-filter: blur(var(--glass-blur));
|
||||
-webkit-backdrop-filter: blur(var(--glass-blur));
|
||||
max-width: 600px;
|
||||
margin: 40px auto;
|
||||
}
|
||||
.file-drop-zone:hover, .file-drop-zone.dragover {
|
||||
border-color: var(--accent-primary);
|
||||
background: rgba(59, 130, 246, 0.05);
|
||||
box-shadow: 0 0 20px rgba(59, 130, 246, 0.1);
|
||||
}
|
||||
.file-drop-zone { border: 2px dashed var(--glass-border); border-radius: var(--radius-lg); padding: 80px 40px; text-align: center; cursor: pointer; transition: all var(--transition-normal); background: var(--glass-bg); backdrop-filter: blur(var(--glass-blur)); -webkit-backdrop-filter: blur(var(--glass-blur)); max-width: 600px; margin: 40px auto; }
|
||||
.file-drop-zone:hover, .file-drop-zone.dragover { border-color: var(--accent-primary); background: rgba(59, 130, 246, 0.05); box-shadow: 0 0 20px rgba(59, 130, 246, 0.1); }
|
||||
.drop-icon { color: var(--text-muted); margin-bottom: 8px; }
|
||||
.drop-text { font-size: 15px; color: var(--text-primary); margin-bottom: 4px; }
|
||||
.drop-hint { font-size: 12px; color: var(--text-muted); }
|
||||
|
||||
.image-workspace {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 320px;
|
||||
gap: 20px;
|
||||
height: calc(100vh - 160px);
|
||||
max-height: 800px;
|
||||
}
|
||||
.image-canvas-area {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
background: var(--glass-bg);
|
||||
backdrop-filter: blur(var(--glass-blur));
|
||||
-webkit-backdrop-filter: blur(var(--glass-blur));
|
||||
border: 1px solid var(--glass-border);
|
||||
border-radius: var(--radius-lg);
|
||||
overflow: hidden;
|
||||
}
|
||||
.canvas-container {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: #1a1a25;
|
||||
overflow: hidden;
|
||||
min-height: 400px;
|
||||
}
|
||||
.image-workspace { display: grid; grid-template-columns: 1fr 320px; gap: 20px; height: calc(100vh - 160px); max-height: 800px; }
|
||||
.image-canvas-area { display: flex; flex-direction: column; background: var(--glass-bg); backdrop-filter: blur(var(--glass-blur)); -webkit-backdrop-filter: blur(var(--glass-blur)); border: 1px solid var(--glass-border); border-radius: var(--radius-lg); overflow: hidden; }
|
||||
.canvas-container { flex: 1; display: flex; align-items: center; justify-content: center; background: #1a1a25; overflow: hidden; min-height: 400px; }
|
||||
.canvas-wrapper { position: relative; display: inline-block; }
|
||||
.preview-img { max-width: 100%; max-height: 100%; object-fit: contain; }
|
||||
.image-info-bar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
padding: 10px 16px;
|
||||
background: var(--glass-bg);
|
||||
border-top: 1px solid var(--glass-border);
|
||||
font-size: 13px;
|
||||
}
|
||||
.image-info-bar { display: flex; align-items: center; gap: 12px; padding: 10px 16px; background: var(--glass-bg); border-top: 1px solid var(--glass-border); font-size: 13px; }
|
||||
.image-dims { color: var(--text-secondary); font-family: monospace; }
|
||||
.image-filename { color: var(--text-primary); flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
||||
.image-panel {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
overflow-y: auto;
|
||||
background: var(--glass-bg);
|
||||
backdrop-filter: blur(var(--glass-blur));
|
||||
-webkit-backdrop-filter: blur(var(--glass-blur));
|
||||
border: 1px solid var(--glass-border);
|
||||
border-radius: var(--radius-lg);
|
||||
padding: 20px;
|
||||
}
|
||||
.image-panel { display: flex; flex-direction: column; gap: 16px; overflow-y: auto; background: var(--glass-bg); backdrop-filter: blur(var(--glass-blur)); -webkit-backdrop-filter: blur(var(--glass-blur)); border: 1px solid var(--glass-border); border-radius: var(--radius-lg); padding: 20px; }
|
||||
.panel-section { display: flex; flex-direction: column; gap: 4px; }
|
||||
.panel-title {
|
||||
font-size: 14px; font-weight: 600; color: var(--text-primary);
|
||||
margin-bottom: 8px; padding-bottom: 8px; border-bottom: 1px solid var(--glass-border);
|
||||
}
|
||||
.panel-title { font-size: 14px; font-weight: 600; color: var(--text-primary); margin-bottom: 8px; padding-bottom: 8px; border-bottom: 1px solid var(--glass-border); }
|
||||
.param-group { margin-bottom: 12px; }
|
||||
.param-label { font-size: 13px; font-weight: 500; color: var(--text-secondary); margin-bottom: 8px; display: flex; align-items: center; gap: 6px; }
|
||||
.preset-group { display: grid; grid-template-columns: 1fr 1fr; gap: 6px; }
|
||||
.preset-btn { display: flex; flex-direction: column; align-items: center; gap: 2px; padding: 8px 12px; border-radius: var(--radius-sm); cursor: pointer; background: var(--bg-surface); border: 1px solid var(--glass-border); transition: all var(--transition-normal); }
|
||||
.preset-btn:hover { border-color: rgba(255, 255, 255, 0.15); background: rgba(255, 255, 255, 0.05); }
|
||||
.preset-btn.active { background: var(--accent-primary); color: #fff; border-color: var(--accent-primary); }
|
||||
.preset-name { font-size: 13px; font-weight: 600; }
|
||||
.preset-desc { font-size: 11px; opacity: 0.7; }
|
||||
.tag-group { display: flex; gap: 6px; flex-wrap: wrap; }
|
||||
.tag-option {
|
||||
display: flex; align-items: center; gap: 4px; padding: 7px 14px;
|
||||
border-radius: var(--radius-sm); font-size: 13px; font-weight: 500;
|
||||
cursor: pointer; background: var(--bg-surface); color: var(--text-secondary);
|
||||
border: 1px solid var(--glass-border); transition: all var(--transition-normal); user-select: none;
|
||||
}
|
||||
.tag-option { display: flex; align-items: center; gap: 4px; padding: 7px 14px; border-radius: var(--radius-sm); font-size: 13px; font-weight: 500; cursor: pointer; background: var(--bg-surface); color: var(--text-secondary); border: 1px solid var(--glass-border); transition: all var(--transition-normal); user-select: none; }
|
||||
.tag-option:hover { color: var(--text-primary); border-color: rgba(255, 255, 255, 0.15); background: rgba(255, 255, 255, 0.05); }
|
||||
.tag-option.active { background: var(--accent-primary); color: #ffffff; border-color: var(--accent-primary); box-shadow: 0 2px 8px rgba(59, 130, 246, 0.3); }
|
||||
.slider-hint { display: flex; justify-content: space-between; font-size: 11px; color: var(--text-muted); margin-top: 4px; }
|
||||
.aspect-lock-btn {
|
||||
display: inline-flex; align-items: center; gap: 6px; padding: 6px 12px;
|
||||
border-radius: var(--radius-sm); font-size: 12px; font-weight: 500;
|
||||
cursor: pointer; background: var(--bg-surface); border: 1px solid var(--glass-border);
|
||||
color: var(--text-secondary); transition: all var(--transition-normal);
|
||||
}
|
||||
.aspect-lock-btn { display: inline-flex; align-items: center; gap: 6px; padding: 6px 12px; border-radius: var(--radius-sm); font-size: 12px; font-weight: 500; cursor: pointer; background: var(--bg-surface); border: 1px solid var(--glass-border); color: var(--text-secondary); transition: all var(--transition-normal); }
|
||||
.aspect-lock-btn.locked { background: var(--accent-primary); color: #fff; border-color: var(--accent-primary); }
|
||||
.size-inputs { display: flex; align-items: center; gap: 8px; }
|
||||
.size-sep { color: var(--text-muted); font-size: 14px; }
|
||||
.size-unit { color: var(--text-muted); font-size: 13px; }
|
||||
.output-row { display: flex; gap: 8px; }
|
||||
.output-row .el-input { flex: 1; }
|
||||
.process-btn {
|
||||
width: 100%; height: 48px; font-size: 15px; font-weight: 600;
|
||||
border-radius: var(--radius-md); background: var(--gradient-primary); border: none;
|
||||
transition: all var(--transition-normal);
|
||||
}
|
||||
.process-btn { width: 100%; height: 48px; font-size: 15px; font-weight: 600; border-radius: var(--radius-md); background: var(--gradient-primary); border: none; transition: all var(--transition-normal); }
|
||||
.process-btn:hover:not(:disabled) { box-shadow: 0 4px 20px rgba(59, 130, 246, 0.4); transform: translateY(-1px); }
|
||||
.process-btn:active:not(:disabled) { transform: translateY(0) scale(0.98); }
|
||||
.estimated-size {
|
||||
margin-top: 8px; padding: 8px 12px;
|
||||
background: rgba(59, 130, 246, 0.08); border-radius: var(--radius-sm);
|
||||
font-size: 13px; color: var(--text-secondary);
|
||||
}
|
||||
.estimated-label { font-weight: 500; }
|
||||
</style>
|
||||
|
||||
102
frontend/src/components/image/ImageToPdfTool.vue
Normal file
102
frontend/src/components/image/ImageToPdfTool.vue
Normal file
@@ -0,0 +1,102 @@
|
||||
<script setup>
|
||||
import { ref } from 'vue'
|
||||
import { ElMessage } from 'element-plus'
|
||||
|
||||
const props = defineProps({
|
||||
filePath: String,
|
||||
filePreview: String,
|
||||
imageInfo: Object,
|
||||
originalWidth: Number,
|
||||
originalHeight: Number,
|
||||
processing: Boolean,
|
||||
resultInfo: Object,
|
||||
outputPath: String,
|
||||
config: Object,
|
||||
})
|
||||
|
||||
const emit = defineEmits(['update:outputPath', 'process', 'selectFile'])
|
||||
|
||||
function buildRequest() {
|
||||
return { inputPath: props.filePath, outputPath: props.outputPath, format: 'toPdf' }
|
||||
}
|
||||
function onProcess() { emit('process', buildRequest()) }
|
||||
function getFileName() { return props.filePath ? props.filePath.split(/[/\\]/).pop() : '' }
|
||||
function formatSize(bytes) {
|
||||
if (!bytes) return '0 B'
|
||||
const k = 1024
|
||||
const sizes = ['B', 'KB', 'MB', 'GB']
|
||||
const i = Math.floor(Math.log(bytes) / Math.log(k))
|
||||
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i]
|
||||
}
|
||||
function onDragOver(e) { e.preventDefault(); e.currentTarget.classList.add('dragover') }
|
||||
function onDragLeave(e) { e.currentTarget.classList.remove('dragover') }
|
||||
function onDrop(e) {
|
||||
e.preventDefault(); e.currentTarget.classList.remove('dragover')
|
||||
if (e.dataTransfer?.files?.length) {
|
||||
const ext = e.dataTransfer.files[0].name.split('.').pop().toLowerCase()
|
||||
if (!['jpg', 'jpeg', 'png', 'gif', 'bmp', 'tiff', 'tif', 'webp', 'ico'].includes(ext)) {
|
||||
ElMessage.warning('请选择图片文件'); return
|
||||
}
|
||||
emit('selectFile', e.dataTransfer.files[0].path)
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div v-if="!filePath" class="file-drop-zone" @click="$emit('selectFile')" @dragover="onDragOver" @dragleave="onDragLeave" @drop="onDrop">
|
||||
<el-icon :size="40" class="drop-icon"><UploadFilled /></el-icon>
|
||||
<div class="drop-text">点击选择图片</div>
|
||||
<div class="drop-hint">支持拖拽图片到此处</div>
|
||||
</div>
|
||||
|
||||
<div v-else class="tool-main">
|
||||
<div class="file-selected-bar" @click="$emit('selectFile')">
|
||||
<img v-if="filePreview" :src="filePreview" class="file-preview-img" />
|
||||
<div class="file-preview-info">
|
||||
<div class="file-name">{{ getFileName() }}</div>
|
||||
<div v-if="imageInfo" class="file-meta">{{ imageInfo.width }}×{{ imageInfo.height }} · {{ formatSize(imageInfo.size) }}</div>
|
||||
</div>
|
||||
<el-button type="danger" text @click.stop="$emit('selectFile')"><el-icon><Delete /></el-icon></el-button>
|
||||
</div>
|
||||
|
||||
<div class="params-section">
|
||||
<div class="tip-text">将图片转换为 PDF 文档,自动适配 A4 页面大小并居中</div>
|
||||
</div>
|
||||
|
||||
<div class="output-section">
|
||||
<div class="output-header"><span class="param-label">输出路径</span></div>
|
||||
<div class="output-row">
|
||||
<el-input :value="outputPath" :placeholder="config?.defaultOutputDir ? '默认: ' + config.defaultOutputDir : '默认: 与输入文件同目录'" readonly />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<el-button class="process-btn" type="primary" size="large" :loading="processing" :disabled="!filePath" @click="onProcess">
|
||||
<el-icon v-if="!processing"><Check /></el-icon>{{ processing ? '转换中...' : '开始转换' }}
|
||||
</el-button>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.file-drop-zone { border: 2px dashed var(--glass-border); border-radius: var(--radius-lg); padding: 80px 40px; text-align: center; cursor: pointer; transition: all var(--transition-normal); background: var(--glass-bg); backdrop-filter: blur(var(--glass-blur)); -webkit-backdrop-filter: blur(var(--glass-blur)); max-width: 600px; margin: 40px auto; }
|
||||
.file-drop-zone:hover, .file-drop-zone.dragover { border-color: var(--accent-primary); background: rgba(59, 130, 246, 0.05); box-shadow: 0 0 20px rgba(59, 130, 246, 0.1); }
|
||||
.drop-icon { color: var(--text-muted); margin-bottom: 8px; }
|
||||
.drop-text { font-size: 15px; color: var(--text-primary); margin-bottom: 4px; }
|
||||
.drop-hint { font-size: 12px; color: var(--text-muted); }
|
||||
.tool-main { max-width: 900px; margin: 0 auto; }
|
||||
.file-selected-bar { display: flex; align-items: center; gap: 14px; padding: 12px 16px; border: 1px solid var(--glass-border); border-radius: var(--radius-lg); margin-bottom: 24px; background: var(--glass-bg); backdrop-filter: blur(var(--glass-blur)); -webkit-backdrop-filter: blur(var(--glass-blur)); cursor: pointer; transition: all var(--transition-normal); }
|
||||
.file-selected-bar:hover { border-color: var(--accent-primary); }
|
||||
.file-preview-img { width: 48px; height: 48px; object-fit: cover; border-radius: var(--radius-sm); }
|
||||
.file-preview-info { flex: 1; min-width: 0; }
|
||||
.file-name { font-size: 14px; font-weight: 500; color: var(--text-primary); }
|
||||
.file-meta { font-size: 12px; color: var(--text-muted); margin-top: 2px; }
|
||||
.params-section { margin-bottom: 20px; }
|
||||
.tip-text { font-size: 13px; color: var(--text-secondary); padding: 12px 16px; background: rgba(59, 130, 246, 0.05); border-radius: var(--radius-sm); border: 1px solid rgba(59, 130, 246, 0.1); }
|
||||
.output-section { margin-bottom: 24px; }
|
||||
.output-header { display: flex; align-items: center; justify-content: space-between; margin-bottom: 8px; }
|
||||
.param-label { font-size: 13px; font-weight: 500; color: var(--text-secondary); margin-bottom: 8px; display: flex; align-items: center; gap: 6px; }
|
||||
.output-row { display: flex; gap: 8px; }
|
||||
.output-row .el-input { flex: 1; }
|
||||
.process-btn { width: 100%; height: 48px; font-size: 15px; font-weight: 600; border-radius: var(--radius-md); background: var(--gradient-primary); border: none; transition: all var(--transition-normal); }
|
||||
.process-btn:hover:not(:disabled) { box-shadow: 0 4px 20px rgba(59, 130, 246, 0.4); transform: translateY(-1px); }
|
||||
.process-btn:active:not(:disabled) { transform: translateY(0) scale(0.98); }
|
||||
</style>
|
||||
113
frontend/src/components/pdf/PdfCompressTool.vue
Normal file
113
frontend/src/components/pdf/PdfCompressTool.vue
Normal file
@@ -0,0 +1,113 @@
|
||||
<script setup>
|
||||
import { ref } from 'vue'
|
||||
import { ElMessage } from 'element-plus'
|
||||
|
||||
const props = defineProps({
|
||||
filePath: String,
|
||||
processing: Boolean,
|
||||
resultInfo: Object,
|
||||
outputPath: String,
|
||||
config: Object,
|
||||
})
|
||||
|
||||
const emit = defineEmits(['process', 'selectFile'])
|
||||
|
||||
const optimizeLevel = ref('medium')
|
||||
|
||||
function buildRequest() {
|
||||
return {
|
||||
inputPath: props.filePath,
|
||||
outputPath: props.outputPath,
|
||||
format: 'compress',
|
||||
optimizeLevel: optimizeLevel.value,
|
||||
}
|
||||
}
|
||||
|
||||
function onProcess() { emit('process', buildRequest()) }
|
||||
function getFileName() { return props.filePath ? props.filePath.split(/[/\\]/).pop() : '' }
|
||||
function onDragOver(e) { e.preventDefault(); e.currentTarget.classList.add('dragover') }
|
||||
function onDragLeave(e) { e.currentTarget.classList.remove('dragover') }
|
||||
function onDrop(e) {
|
||||
e.preventDefault(); e.currentTarget.classList.remove('dragover')
|
||||
if (e.dataTransfer?.files?.length) {
|
||||
const ext = e.dataTransfer.files[0].name.split('.').pop().toLowerCase()
|
||||
if (ext !== 'pdf') { ElMessage.warning('请选择 PDF 文件'); return }
|
||||
emit('selectFile', e.dataTransfer.files[0].path)
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div v-if="!filePath" class="file-drop-zone" @click="$emit('selectFile')" @dragover="onDragOver" @dragleave="onDragLeave" @drop="onDrop">
|
||||
<el-icon :size="40" class="drop-icon"><UploadFilled /></el-icon>
|
||||
<div class="drop-text">点击选择 PDF 文件</div>
|
||||
<div class="drop-hint">支持拖拽 PDF 到此处</div>
|
||||
</div>
|
||||
<div v-else class="pdf-workspace">
|
||||
<div class="pdf-canvas-area">
|
||||
<div class="canvas-container">
|
||||
<div class="canvas-placeholder">
|
||||
<el-icon :size="64" style="color: var(--text-muted)"><Document /></el-icon>
|
||||
<div class="placeholder-name">{{ getFileName() }}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="pdf-info-bar">
|
||||
<span class="pdf-filename">{{ getFileName() }}</span>
|
||||
<el-button type="danger" text size="small" @click="$emit('selectFile')"><el-icon><Delete /></el-icon></el-button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="pdf-panel">
|
||||
<div class="panel-section">
|
||||
<div class="panel-title">PDF 瘦身</div>
|
||||
<div class="param-group">
|
||||
<div class="param-label">优化级别</div>
|
||||
<div class="tag-group">
|
||||
<div class="tag-option" :class="{ active: optimizeLevel === 'light' }" @click="optimizeLevel = 'light'">轻度</div>
|
||||
<div class="tag-option" :class="{ active: optimizeLevel === 'medium' }" @click="optimizeLevel = 'medium'">中等</div>
|
||||
<div class="tag-option" :class="{ active: optimizeLevel === 'heavy' }" @click="optimizeLevel = 'heavy'">深度</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="tip-text">优化将去除重复对象、压缩数据流,减小文件大小</div>
|
||||
</div>
|
||||
<div class="panel-section">
|
||||
<div class="panel-title">输出路径</div>
|
||||
<div class="output-row">
|
||||
<el-input :value="outputPath" :placeholder="config?.defaultOutputDir ? '默认: ' + config.defaultOutputDir : '默认: 与输入文件同目录'" readonly size="small" />
|
||||
</div>
|
||||
</div>
|
||||
<el-button class="process-btn" type="primary" size="large" :loading="processing" :disabled="!filePath" @click="onProcess">
|
||||
<el-icon v-if="!processing"><Check /></el-icon>{{ processing ? '处理中...' : '开始瘦身' }}
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.file-drop-zone { border: 2px dashed var(--glass-border); border-radius: var(--radius-lg); padding: 80px 40px; text-align: center; cursor: pointer; transition: all var(--transition-normal); background: var(--glass-bg); backdrop-filter: blur(var(--glass-blur)); -webkit-backdrop-filter: blur(var(--glass-blur)); max-width: 600px; margin: 40px auto; }
|
||||
.file-drop-zone:hover, .file-drop-zone.dragover { border-color: var(--accent-primary); background: rgba(59, 130, 246, 0.05); box-shadow: 0 0 20px rgba(59, 130, 246, 0.1); }
|
||||
.drop-icon { color: var(--text-muted); margin-bottom: 8px; }
|
||||
.drop-text { font-size: 15px; color: var(--text-primary); margin-bottom: 4px; }
|
||||
.drop-hint { font-size: 12px; color: var(--text-muted); }
|
||||
.pdf-workspace { display: grid; grid-template-columns: 1fr 320px; gap: 20px; height: calc(100vh - 160px); max-height: 800px; }
|
||||
.pdf-canvas-area { display: flex; flex-direction: column; background: var(--glass-bg); backdrop-filter: blur(var(--glass-blur)); -webkit-backdrop-filter: blur(var(--glass-blur)); border: 1px solid var(--glass-border); border-radius: var(--radius-lg); overflow: hidden; }
|
||||
.canvas-container { flex: 1; display: flex; align-items: center; justify-content: center; background: #1a1a25; overflow: hidden; min-height: 400px; }
|
||||
.canvas-placeholder { text-align: center; }
|
||||
.placeholder-name { margin-top: 12px; font-size: 14px; color: var(--text-secondary); max-width: 300px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
||||
.pdf-info-bar { display: flex; align-items: center; gap: 12px; padding: 10px 16px; background: var(--glass-bg); border-top: 1px solid var(--glass-border); font-size: 13px; }
|
||||
.pdf-filename { color: var(--text-primary); flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
||||
.pdf-panel { display: flex; flex-direction: column; gap: 16px; overflow-y: auto; background: var(--glass-bg); backdrop-filter: blur(var(--glass-blur)); -webkit-backdrop-filter: blur(var(--glass-blur)); border: 1px solid var(--glass-border); border-radius: var(--radius-lg); padding: 20px; }
|
||||
.panel-section { display: flex; flex-direction: column; gap: 4px; }
|
||||
.panel-title { font-size: 14px; font-weight: 600; color: var(--text-primary); margin-bottom: 8px; padding-bottom: 8px; border-bottom: 1px solid var(--glass-border); }
|
||||
.param-group { margin-bottom: 12px; }
|
||||
.param-label { font-size: 13px; font-weight: 500; color: var(--text-secondary); margin-bottom: 8px; display: flex; align-items: center; gap: 6px; }
|
||||
.tag-group { display: flex; gap: 6px; flex-wrap: wrap; }
|
||||
.tag-option { display: flex; align-items: center; gap: 4px; padding: 7px 14px; border-radius: var(--radius-sm); font-size: 13px; font-weight: 500; cursor: pointer; background: var(--bg-surface); color: var(--text-secondary); border: 1px solid var(--glass-border); transition: all var(--transition-normal); user-select: none; }
|
||||
.tag-option:hover { color: var(--text-primary); border-color: rgba(255, 255, 255, 0.15); background: rgba(255, 255, 255, 0.05); }
|
||||
.tag-option.active { background: var(--accent-primary); color: #ffffff; border-color: var(--accent-primary); box-shadow: 0 2px 8px rgba(59, 130, 246, 0.3); }
|
||||
.tip-text { font-size: 12px; color: var(--text-muted); margin-top: 4px; }
|
||||
.output-row { display: flex; gap: 8px; }
|
||||
.output-row .el-input { flex: 1; }
|
||||
.process-btn { width: 100%; height: 48px; font-size: 15px; font-weight: 600; border-radius: var(--radius-md); background: var(--gradient-primary); border: none; transition: all var(--transition-normal); }
|
||||
.process-btn:hover:not(:disabled) { box-shadow: 0 4px 20px rgba(59, 130, 246, 0.4); transform: translateY(-1px); }
|
||||
.process-btn:active:not(:disabled) { transform: translateY(0) scale(0.98); }
|
||||
</style>
|
||||
91
frontend/src/components/pdf/PdfExtractTextTool.vue
Normal file
91
frontend/src/components/pdf/PdfExtractTextTool.vue
Normal file
@@ -0,0 +1,91 @@
|
||||
<script setup>
|
||||
import { ref } from 'vue'
|
||||
import { ElMessage } from 'element-plus'
|
||||
|
||||
const props = defineProps({
|
||||
filePath: String,
|
||||
processing: Boolean,
|
||||
resultInfo: Object,
|
||||
outputPath: String,
|
||||
config: Object,
|
||||
})
|
||||
|
||||
const emit = defineEmits(['process', 'selectFile'])
|
||||
|
||||
function buildRequest() {
|
||||
return { inputPath: props.filePath, outputPath: props.outputPath, format: 'toText' }
|
||||
}
|
||||
function onProcess() { emit('process', buildRequest()) }
|
||||
function getFileName() { return props.filePath ? props.filePath.split(/[/\\]/).pop() : '' }
|
||||
function onDragOver(e) { e.preventDefault(); e.currentTarget.classList.add('dragover') }
|
||||
function onDragLeave(e) { e.currentTarget.classList.remove('dragover') }
|
||||
function onDrop(e) {
|
||||
e.preventDefault(); e.currentTarget.classList.remove('dragover')
|
||||
if (e.dataTransfer?.files?.length) {
|
||||
const ext = e.dataTransfer.files[0].name.split('.').pop().toLowerCase()
|
||||
if (ext !== 'pdf') { ElMessage.warning('请选择 PDF 文件'); return }
|
||||
emit('selectFile', e.dataTransfer.files[0].path)
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div v-if="!filePath" class="file-drop-zone" @click="$emit('selectFile')" @dragover="onDragOver" @dragleave="onDragLeave" @drop="onDrop">
|
||||
<el-icon :size="40" class="drop-icon"><UploadFilled /></el-icon>
|
||||
<div class="drop-text">点击选择 PDF 文件</div>
|
||||
<div class="drop-hint">支持拖拽 PDF 到此处</div>
|
||||
</div>
|
||||
<div v-else class="pdf-workspace">
|
||||
<div class="pdf-canvas-area">
|
||||
<div class="canvas-container">
|
||||
<div class="canvas-placeholder">
|
||||
<el-icon :size="64" style="color: var(--text-muted)"><Document /></el-icon>
|
||||
<div class="placeholder-name">{{ getFileName() }}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="pdf-info-bar">
|
||||
<span class="pdf-filename">{{ getFileName() }}</span>
|
||||
<el-button type="danger" text size="small" @click="$emit('selectFile')"><el-icon><Delete /></el-icon></el-button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="pdf-panel">
|
||||
<div class="panel-section">
|
||||
<div class="panel-title">提取文本</div>
|
||||
<div class="tip-text">从 PDF 中提取纯文本内容,保存为 .txt 文件</div>
|
||||
</div>
|
||||
<div class="panel-section">
|
||||
<div class="panel-title">输出路径</div>
|
||||
<div class="output-row">
|
||||
<el-input :value="outputPath" :placeholder="config?.defaultOutputDir ? '默认: ' + config.defaultOutputDir : '默认: 与输入文件同目录'" readonly size="small" />
|
||||
</div>
|
||||
</div>
|
||||
<el-button class="process-btn" type="primary" size="large" :loading="processing" :disabled="!filePath" @click="onProcess">
|
||||
<el-icon v-if="!processing"><Check /></el-icon>{{ processing ? '提取中...' : '提取文本' }}
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.file-drop-zone { border: 2px dashed var(--glass-border); border-radius: var(--radius-lg); padding: 80px 40px; text-align: center; cursor: pointer; transition: all var(--transition-normal); background: var(--glass-bg); backdrop-filter: blur(var(--glass-blur)); -webkit-backdrop-filter: blur(var(--glass-blur)); max-width: 600px; margin: 40px auto; }
|
||||
.file-drop-zone:hover, .file-drop-zone.dragover { border-color: var(--accent-primary); background: rgba(59, 130, 246, 0.05); box-shadow: 0 0 20px rgba(59, 130, 246, 0.1); }
|
||||
.drop-icon { color: var(--text-muted); margin-bottom: 8px; }
|
||||
.drop-text { font-size: 15px; color: var(--text-primary); margin-bottom: 4px; }
|
||||
.drop-hint { font-size: 12px; color: var(--text-muted); }
|
||||
.pdf-workspace { display: grid; grid-template-columns: 1fr 320px; gap: 20px; height: calc(100vh - 160px); max-height: 800px; }
|
||||
.pdf-canvas-area { display: flex; flex-direction: column; background: var(--glass-bg); backdrop-filter: blur(var(--glass-blur)); -webkit-backdrop-filter: blur(var(--glass-blur)); border: 1px solid var(--glass-border); border-radius: var(--radius-lg); overflow: hidden; }
|
||||
.canvas-container { flex: 1; display: flex; align-items: center; justify-content: center; background: #1a1a25; overflow: hidden; min-height: 400px; }
|
||||
.canvas-placeholder { text-align: center; }
|
||||
.placeholder-name { margin-top: 12px; font-size: 14px; color: var(--text-secondary); max-width: 300px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
||||
.pdf-info-bar { display: flex; align-items: center; gap: 12px; padding: 10px 16px; background: var(--glass-bg); border-top: 1px solid var(--glass-border); font-size: 13px; }
|
||||
.pdf-filename { color: var(--text-primary); flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
||||
.pdf-panel { display: flex; flex-direction: column; gap: 16px; overflow-y: auto; background: var(--glass-bg); backdrop-filter: blur(var(--glass-blur)); -webkit-backdrop-filter: blur(var(--glass-blur)); border: 1px solid var(--glass-border); border-radius: var(--radius-lg); padding: 20px; }
|
||||
.panel-section { display: flex; flex-direction: column; gap: 4px; }
|
||||
.panel-title { font-size: 14px; font-weight: 600; color: var(--text-primary); margin-bottom: 8px; padding-bottom: 8px; border-bottom: 1px solid var(--glass-border); }
|
||||
.tip-text { font-size: 12px; color: var(--text-muted); margin-top: 4px; }
|
||||
.output-row { display: flex; gap: 8px; }
|
||||
.output-row .el-input { flex: 1; }
|
||||
.process-btn { width: 100%; height: 48px; font-size: 15px; font-weight: 600; border-radius: var(--radius-md); background: var(--gradient-primary); border: none; transition: all var(--transition-normal); }
|
||||
.process-btn:hover:not(:disabled) { box-shadow: 0 4px 20px rgba(59, 130, 246, 0.4); transform: translateY(-1px); }
|
||||
.process-btn:active:not(:disabled) { transform: translateY(0) scale(0.98); }
|
||||
</style>
|
||||
92
frontend/src/components/pdf/PdfFromTextTool.vue
Normal file
92
frontend/src/components/pdf/PdfFromTextTool.vue
Normal file
@@ -0,0 +1,92 @@
|
||||
<script setup>
|
||||
import { ref } from 'vue'
|
||||
import { ElMessage } from 'element-plus'
|
||||
|
||||
const props = defineProps({
|
||||
filePath: String,
|
||||
processing: Boolean,
|
||||
resultInfo: Object,
|
||||
outputPath: String,
|
||||
config: Object,
|
||||
})
|
||||
|
||||
const emit = defineEmits(['process', 'selectFile'])
|
||||
|
||||
function buildRequest() {
|
||||
const ext = props.filePath ? props.filePath.split('.').pop().toLowerCase() : ''
|
||||
return { inputPath: props.filePath, outputPath: props.outputPath, format: ext === 'md' ? 'toMarkdown' : ext === 'html' ? 'toHtml' : 'toText' }
|
||||
}
|
||||
function onProcess() { emit('process', buildRequest()) }
|
||||
function getFileName() { return props.filePath ? props.filePath.split(/[/\\]/).pop() : '' }
|
||||
function onDragOver(e) { e.preventDefault(); e.currentTarget.classList.add('dragover') }
|
||||
function onDragLeave(e) { e.currentTarget.classList.remove('dragover') }
|
||||
function onDrop(e) {
|
||||
e.preventDefault(); e.currentTarget.classList.remove('dragover')
|
||||
if (e.dataTransfer?.files?.length) {
|
||||
const ext = e.dataTransfer.files[0].name.split('.').pop().toLowerCase()
|
||||
if (!['txt', 'md', 'html'].includes(ext)) { ElMessage.warning('请选择文本/Markdown/HTML 文件'); return }
|
||||
emit('selectFile', e.dataTransfer.files[0].path)
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div v-if="!filePath" class="file-drop-zone" @click="$emit('selectFile')" @dragover="onDragOver" @dragleave="onDragLeave" @drop="onDrop">
|
||||
<el-icon :size="40" class="drop-icon"><UploadFilled /></el-icon>
|
||||
<div class="drop-text">点击选择文本文件</div>
|
||||
<div class="drop-hint">支持 .txt / .md / .html 文件</div>
|
||||
</div>
|
||||
<div v-else class="pdf-workspace">
|
||||
<div class="pdf-canvas-area">
|
||||
<div class="canvas-container">
|
||||
<div class="canvas-placeholder">
|
||||
<el-icon :size="64" style="color: var(--text-muted)"><Document /></el-icon>
|
||||
<div class="placeholder-name">{{ getFileName() }}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="pdf-info-bar">
|
||||
<span class="pdf-filename">{{ getFileName() }}</span>
|
||||
<el-button type="danger" text size="small" @click="$emit('selectFile')"><el-icon><Delete /></el-icon></el-button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="pdf-panel">
|
||||
<div class="panel-section">
|
||||
<div class="panel-title">文本转 PDF</div>
|
||||
<div class="tip-text">将文本/Markdown/HTML 文件转换为 PDF 文档</div>
|
||||
</div>
|
||||
<div class="panel-section">
|
||||
<div class="panel-title">输出路径</div>
|
||||
<div class="output-row">
|
||||
<el-input :value="outputPath" :placeholder="config?.defaultOutputDir ? '默认: ' + config.defaultOutputDir : '默认: 与输入文件同目录'" readonly size="small" />
|
||||
</div>
|
||||
</div>
|
||||
<el-button class="process-btn" type="primary" size="large" :loading="processing" :disabled="!filePath" @click="onProcess">
|
||||
<el-icon v-if="!processing"><Check /></el-icon>{{ processing ? '转换中...' : '开始转换' }}
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.file-drop-zone { border: 2px dashed var(--glass-border); border-radius: var(--radius-lg); padding: 80px 40px; text-align: center; cursor: pointer; transition: all var(--transition-normal); background: var(--glass-bg); backdrop-filter: blur(var(--glass-blur)); -webkit-backdrop-filter: blur(var(--glass-blur)); max-width: 600px; margin: 40px auto; }
|
||||
.file-drop-zone:hover, .file-drop-zone.dragover { border-color: var(--accent-primary); background: rgba(59, 130, 246, 0.05); box-shadow: 0 0 20px rgba(59, 130, 246, 0.1); }
|
||||
.drop-icon { color: var(--text-muted); margin-bottom: 8px; }
|
||||
.drop-text { font-size: 15px; color: var(--text-primary); margin-bottom: 4px; }
|
||||
.drop-hint { font-size: 12px; color: var(--text-muted); }
|
||||
.pdf-workspace { display: grid; grid-template-columns: 1fr 320px; gap: 20px; height: calc(100vh - 160px); max-height: 800px; }
|
||||
.pdf-canvas-area { display: flex; flex-direction: column; background: var(--glass-bg); backdrop-filter: blur(var(--glass-blur)); -webkit-backdrop-filter: blur(var(--glass-blur)); border: 1px solid var(--glass-border); border-radius: var(--radius-lg); overflow: hidden; }
|
||||
.canvas-container { flex: 1; display: flex; align-items: center; justify-content: center; background: #1a1a25; overflow: hidden; min-height: 400px; }
|
||||
.canvas-placeholder { text-align: center; }
|
||||
.placeholder-name { margin-top: 12px; font-size: 14px; color: var(--text-secondary); max-width: 300px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
||||
.pdf-info-bar { display: flex; align-items: center; gap: 12px; padding: 10px 16px; background: var(--glass-bg); border-top: 1px solid var(--glass-border); font-size: 13px; }
|
||||
.pdf-filename { color: var(--text-primary); flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
||||
.pdf-panel { display: flex; flex-direction: column; gap: 16px; overflow-y: auto; background: var(--glass-bg); backdrop-filter: blur(var(--glass-blur)); -webkit-backdrop-filter: blur(var(--glass-blur)); border: 1px solid var(--glass-border); border-radius: var(--radius-lg); padding: 20px; }
|
||||
.panel-section { display: flex; flex-direction: column; gap: 4px; }
|
||||
.panel-title { font-size: 14px; font-weight: 600; color: var(--text-primary); margin-bottom: 8px; padding-bottom: 8px; border-bottom: 1px solid var(--glass-border); }
|
||||
.tip-text { font-size: 12px; color: var(--text-muted); margin-top: 4px; }
|
||||
.output-row { display: flex; gap: 8px; }
|
||||
.output-row .el-input { flex: 1; }
|
||||
.process-btn { width: 100%; height: 48px; font-size: 15px; font-weight: 600; border-radius: var(--radius-md); background: var(--gradient-primary); border: none; transition: all var(--transition-normal); }
|
||||
.process-btn:hover:not(:disabled) { box-shadow: 0 4px 20px rgba(59, 130, 246, 0.4); transform: translateY(-1px); }
|
||||
.process-btn:active:not(:disabled) { transform: translateY(0) scale(0.98); }
|
||||
</style>
|
||||
91
frontend/src/components/pdf/PdfMergeTool.vue
Normal file
91
frontend/src/components/pdf/PdfMergeTool.vue
Normal file
@@ -0,0 +1,91 @@
|
||||
<script setup>
|
||||
import { ref } from 'vue'
|
||||
import { ElMessage } from 'element-plus'
|
||||
|
||||
const props = defineProps({
|
||||
filePath: String,
|
||||
processing: Boolean,
|
||||
resultInfo: Object,
|
||||
outputPath: String,
|
||||
config: Object,
|
||||
})
|
||||
|
||||
const emit = defineEmits(['process', 'selectFile'])
|
||||
|
||||
function buildRequest() {
|
||||
return { inputPath: props.filePath, outputPath: props.outputPath, format: 'merge', inputPaths: [props.filePath] }
|
||||
}
|
||||
function onProcess() { emit('process', buildRequest()) }
|
||||
function getFileName() { return props.filePath ? props.filePath.split(/[/\\]/).pop() : '' }
|
||||
function onDragOver(e) { e.preventDefault(); e.currentTarget.classList.add('dragover') }
|
||||
function onDragLeave(e) { e.currentTarget.classList.remove('dragover') }
|
||||
function onDrop(e) {
|
||||
e.preventDefault(); e.currentTarget.classList.remove('dragover')
|
||||
if (e.dataTransfer?.files?.length) {
|
||||
const ext = e.dataTransfer.files[0].name.split('.').pop().toLowerCase()
|
||||
if (ext !== 'pdf') { ElMessage.warning('请选择 PDF 文件'); return }
|
||||
emit('selectFile', e.dataTransfer.files[0].path)
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div v-if="!filePath" class="file-drop-zone" @click="$emit('selectFile')" @dragover="onDragOver" @dragleave="onDragLeave" @drop="onDrop">
|
||||
<el-icon :size="40" class="drop-icon"><UploadFilled /></el-icon>
|
||||
<div class="drop-text">点击选择 PDF 文件</div>
|
||||
<div class="drop-hint">支持拖拽 PDF 到此处</div>
|
||||
</div>
|
||||
<div v-else class="pdf-workspace">
|
||||
<div class="pdf-canvas-area">
|
||||
<div class="canvas-container">
|
||||
<div class="canvas-placeholder">
|
||||
<el-icon :size="64" style="color: var(--text-muted)"><Document /></el-icon>
|
||||
<div class="placeholder-name">{{ getFileName() }}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="pdf-info-bar">
|
||||
<span class="pdf-filename">{{ getFileName() }}</span>
|
||||
<el-button type="danger" text size="small" @click="$emit('selectFile')"><el-icon><Delete /></el-icon></el-button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="pdf-panel">
|
||||
<div class="panel-section">
|
||||
<div class="panel-title">合并 PDF</div>
|
||||
<div class="tip-text">将当前 PDF 与其他 PDF 合并为一个文件</div>
|
||||
</div>
|
||||
<div class="panel-section">
|
||||
<div class="panel-title">输出路径</div>
|
||||
<div class="output-row">
|
||||
<el-input :value="outputPath" :placeholder="config?.defaultOutputDir ? '默认: ' + config.defaultOutputDir : '默认: 与输入文件同目录'" readonly size="small" />
|
||||
</div>
|
||||
</div>
|
||||
<el-button class="process-btn" type="primary" size="large" :loading="processing" :disabled="!filePath" @click="onProcess">
|
||||
<el-icon v-if="!processing"><Check /></el-icon>{{ processing ? '合并中...' : '开始合并' }}
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.file-drop-zone { border: 2px dashed var(--glass-border); border-radius: var(--radius-lg); padding: 80px 40px; text-align: center; cursor: pointer; transition: all var(--transition-normal); background: var(--glass-bg); backdrop-filter: blur(var(--glass-blur)); -webkit-backdrop-filter: blur(var(--glass-blur)); max-width: 600px; margin: 40px auto; }
|
||||
.file-drop-zone:hover, .file-drop-zone.dragover { border-color: var(--accent-primary); background: rgba(59, 130, 246, 0.05); box-shadow: 0 0 20px rgba(59, 130, 246, 0.1); }
|
||||
.drop-icon { color: var(--text-muted); margin-bottom: 8px; }
|
||||
.drop-text { font-size: 15px; color: var(--text-primary); margin-bottom: 4px; }
|
||||
.drop-hint { font-size: 12px; color: var(--text-muted); }
|
||||
.pdf-workspace { display: grid; grid-template-columns: 1fr 320px; gap: 20px; height: calc(100vh - 160px); max-height: 800px; }
|
||||
.pdf-canvas-area { display: flex; flex-direction: column; background: var(--glass-bg); backdrop-filter: blur(var(--glass-blur)); -webkit-backdrop-filter: blur(var(--glass-blur)); border: 1px solid var(--glass-border); border-radius: var(--radius-lg); overflow: hidden; }
|
||||
.canvas-container { flex: 1; display: flex; align-items: center; justify-content: center; background: #1a1a25; overflow: hidden; min-height: 400px; }
|
||||
.canvas-placeholder { text-align: center; }
|
||||
.placeholder-name { margin-top: 12px; font-size: 14px; color: var(--text-secondary); max-width: 300px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
||||
.pdf-info-bar { display: flex; align-items: center; gap: 12px; padding: 10px 16px; background: var(--glass-bg); border-top: 1px solid var(--glass-border); font-size: 13px; }
|
||||
.pdf-filename { color: var(--text-primary); flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
||||
.pdf-panel { display: flex; flex-direction: column; gap: 16px; overflow-y: auto; background: var(--glass-bg); backdrop-filter: blur(var(--glass-blur)); -webkit-backdrop-filter: blur(var(--glass-blur)); border: 1px solid var(--glass-border); border-radius: var(--radius-lg); padding: 20px; }
|
||||
.panel-section { display: flex; flex-direction: column; gap: 4px; }
|
||||
.panel-title { font-size: 14px; font-weight: 600; color: var(--text-primary); margin-bottom: 8px; padding-bottom: 8px; border-bottom: 1px solid var(--glass-border); }
|
||||
.tip-text { font-size: 12px; color: var(--text-muted); margin-top: 4px; }
|
||||
.output-row { display: flex; gap: 8px; }
|
||||
.output-row .el-input { flex: 1; }
|
||||
.process-btn { width: 100%; height: 48px; font-size: 15px; font-weight: 600; border-radius: var(--radius-md); background: var(--gradient-primary); border: none; transition: all var(--transition-normal); }
|
||||
.process-btn:hover:not(:disabled) { box-shadow: 0 4px 20px rgba(59, 130, 246, 0.4); transform: translateY(-1px); }
|
||||
.process-btn:active:not(:disabled) { transform: translateY(0) scale(0.98); }
|
||||
</style>
|
||||
105
frontend/src/components/pdf/PdfRotateTool.vue
Normal file
105
frontend/src/components/pdf/PdfRotateTool.vue
Normal file
@@ -0,0 +1,105 @@
|
||||
<script setup>
|
||||
import { ref } from 'vue'
|
||||
import { ElMessage } from 'element-plus'
|
||||
|
||||
const props = defineProps({
|
||||
filePath: String,
|
||||
processing: Boolean,
|
||||
resultInfo: Object,
|
||||
outputPath: String,
|
||||
config: Object,
|
||||
})
|
||||
|
||||
const emit = defineEmits(['process', 'selectFile'])
|
||||
|
||||
const angle = ref(90)
|
||||
|
||||
function buildRequest() {
|
||||
return { inputPath: props.filePath, outputPath: props.outputPath, format: 'rotate', angle: angle.value }
|
||||
}
|
||||
function onProcess() { emit('process', buildRequest()) }
|
||||
function getFileName() { return props.filePath ? props.filePath.split(/[/\\]/).pop() : '' }
|
||||
function onDragOver(e) { e.preventDefault(); e.currentTarget.classList.add('dragover') }
|
||||
function onDragLeave(e) { e.currentTarget.classList.remove('dragover') }
|
||||
function onDrop(e) {
|
||||
e.preventDefault(); e.currentTarget.classList.remove('dragover')
|
||||
if (e.dataTransfer?.files?.length) {
|
||||
const ext = e.dataTransfer.files[0].name.split('.').pop().toLowerCase()
|
||||
if (ext !== 'pdf') { ElMessage.warning('请选择 PDF 文件'); return }
|
||||
emit('selectFile', e.dataTransfer.files[0].path)
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div v-if="!filePath" class="file-drop-zone" @click="$emit('selectFile')" @dragover="onDragOver" @dragleave="onDragLeave" @drop="onDrop">
|
||||
<el-icon :size="40" class="drop-icon"><UploadFilled /></el-icon>
|
||||
<div class="drop-text">点击选择 PDF 文件</div>
|
||||
<div class="drop-hint">支持拖拽 PDF 到此处</div>
|
||||
</div>
|
||||
<div v-else class="pdf-workspace">
|
||||
<div class="pdf-canvas-area">
|
||||
<div class="canvas-container">
|
||||
<div class="canvas-placeholder">
|
||||
<el-icon :size="64" style="color: var(--text-muted)"><Document /></el-icon>
|
||||
<div class="placeholder-name">{{ getFileName() }}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="pdf-info-bar">
|
||||
<span class="pdf-filename">{{ getFileName() }}</span>
|
||||
<el-button type="danger" text size="small" @click="$emit('selectFile')"><el-icon><Delete /></el-icon></el-button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="pdf-panel">
|
||||
<div class="panel-section">
|
||||
<div class="panel-title">旋转设置</div>
|
||||
<div class="param-group">
|
||||
<div class="param-label">旋转角度: {{ angle }}°</div>
|
||||
<div class="tag-group">
|
||||
<div class="tag-option" :class="{ active: angle === 90 }" @click="angle = 90">90°</div>
|
||||
<div class="tag-option" :class="{ active: angle === 180 }" @click="angle = 180">180°</div>
|
||||
<div class="tag-option" :class="{ active: angle === 270 }" @click="angle = 270">270°</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="panel-section">
|
||||
<div class="panel-title">输出路径</div>
|
||||
<div class="output-row">
|
||||
<el-input :value="outputPath" :placeholder="config?.defaultOutputDir ? '默认: ' + config.defaultOutputDir : '默认: 与输入文件同目录'" readonly size="small" />
|
||||
</div>
|
||||
</div>
|
||||
<el-button class="process-btn" type="primary" size="large" :loading="processing" :disabled="!filePath" @click="onProcess">
|
||||
<el-icon v-if="!processing"><Check /></el-icon>{{ processing ? '处理中...' : '开始旋转' }}
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.file-drop-zone { border: 2px dashed var(--glass-border); border-radius: var(--radius-lg); padding: 80px 40px; text-align: center; cursor: pointer; transition: all var(--transition-normal); background: var(--glass-bg); backdrop-filter: blur(var(--glass-blur)); -webkit-backdrop-filter: blur(var(--glass-blur)); max-width: 600px; margin: 40px auto; }
|
||||
.file-drop-zone:hover, .file-drop-zone.dragover { border-color: var(--accent-primary); background: rgba(59, 130, 246, 0.05); box-shadow: 0 0 20px rgba(59, 130, 246, 0.1); }
|
||||
.drop-icon { color: var(--text-muted); margin-bottom: 8px; }
|
||||
.drop-text { font-size: 15px; color: var(--text-primary); margin-bottom: 4px; }
|
||||
.drop-hint { font-size: 12px; color: var(--text-muted); }
|
||||
.pdf-workspace { display: grid; grid-template-columns: 1fr 320px; gap: 20px; height: calc(100vh - 160px); max-height: 800px; }
|
||||
.pdf-canvas-area { display: flex; flex-direction: column; background: var(--glass-bg); backdrop-filter: blur(var(--glass-blur)); -webkit-backdrop-filter: blur(var(--glass-blur)); border: 1px solid var(--glass-border); border-radius: var(--radius-lg); overflow: hidden; }
|
||||
.canvas-container { flex: 1; display: flex; align-items: center; justify-content: center; background: #1a1a25; overflow: hidden; min-height: 400px; }
|
||||
.canvas-placeholder { text-align: center; }
|
||||
.placeholder-name { margin-top: 12px; font-size: 14px; color: var(--text-secondary); max-width: 300px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
||||
.pdf-info-bar { display: flex; align-items: center; gap: 12px; padding: 10px 16px; background: var(--glass-bg); border-top: 1px solid var(--glass-border); font-size: 13px; }
|
||||
.pdf-filename { color: var(--text-primary); flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
||||
.pdf-panel { display: flex; flex-direction: column; gap: 16px; overflow-y: auto; background: var(--glass-bg); backdrop-filter: blur(var(--glass-blur)); -webkit-backdrop-filter: blur(var(--glass-blur)); border: 1px solid var(--glass-border); border-radius: var(--radius-lg); padding: 20px; }
|
||||
.panel-section { display: flex; flex-direction: column; gap: 4px; }
|
||||
.panel-title { font-size: 14px; font-weight: 600; color: var(--text-primary); margin-bottom: 8px; padding-bottom: 8px; border-bottom: 1px solid var(--glass-border); }
|
||||
.param-group { margin-bottom: 12px; }
|
||||
.param-label { font-size: 13px; font-weight: 500; color: var(--text-secondary); margin-bottom: 8px; display: flex; align-items: center; gap: 6px; }
|
||||
.tag-group { display: flex; gap: 6px; flex-wrap: wrap; }
|
||||
.tag-option { display: flex; align-items: center; gap: 4px; padding: 7px 14px; border-radius: var(--radius-sm); font-size: 13px; font-weight: 500; cursor: pointer; background: var(--bg-surface); color: var(--text-secondary); border: 1px solid var(--glass-border); transition: all var(--transition-normal); user-select: none; }
|
||||
.tag-option:hover { color: var(--text-primary); border-color: rgba(255, 255, 255, 0.15); background: rgba(255, 255, 255, 0.05); }
|
||||
.tag-option.active { background: var(--accent-primary); color: #ffffff; border-color: var(--accent-primary); box-shadow: 0 2px 8px rgba(59, 130, 246, 0.3); }
|
||||
.output-row { display: flex; gap: 8px; }
|
||||
.output-row .el-input { flex: 1; }
|
||||
.process-btn { width: 100%; height: 48px; font-size: 15px; font-weight: 600; border-radius: var(--radius-md); background: var(--gradient-primary); border: none; transition: all var(--transition-normal); }
|
||||
.process-btn:hover:not(:disabled) { box-shadow: 0 4px 20px rgba(59, 130, 246, 0.4); transform: translateY(-1px); }
|
||||
.process-btn:active:not(:disabled) { transform: translateY(0) scale(0.98); }
|
||||
</style>
|
||||
99
frontend/src/components/pdf/PdfSplitTool.vue
Normal file
99
frontend/src/components/pdf/PdfSplitTool.vue
Normal file
@@ -0,0 +1,99 @@
|
||||
<script setup>
|
||||
import { ref } from 'vue'
|
||||
import { ElMessage } from 'element-plus'
|
||||
|
||||
const props = defineProps({
|
||||
filePath: String,
|
||||
processing: Boolean,
|
||||
resultInfo: Object,
|
||||
outputPath: String,
|
||||
config: Object,
|
||||
})
|
||||
|
||||
const emit = defineEmits(['process', 'selectFile'])
|
||||
|
||||
const pageRanges = ref('')
|
||||
|
||||
function buildRequest() {
|
||||
return { inputPath: props.filePath, outputPath: props.outputPath, format: 'split', pageRanges: pageRanges.value }
|
||||
}
|
||||
function onProcess() { emit('process', buildRequest()) }
|
||||
function getFileName() { return props.filePath ? props.filePath.split(/[/\\]/).pop() : '' }
|
||||
function onDragOver(e) { e.preventDefault(); e.currentTarget.classList.add('dragover') }
|
||||
function onDragLeave(e) { e.currentTarget.classList.remove('dragover') }
|
||||
function onDrop(e) {
|
||||
e.preventDefault(); e.currentTarget.classList.remove('dragover')
|
||||
if (e.dataTransfer?.files?.length) {
|
||||
const ext = e.dataTransfer.files[0].name.split('.').pop().toLowerCase()
|
||||
if (ext !== 'pdf') { ElMessage.warning('请选择 PDF 文件'); return }
|
||||
emit('selectFile', e.dataTransfer.files[0].path)
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div v-if="!filePath" class="file-drop-zone" @click="$emit('selectFile')" @dragover="onDragOver" @dragleave="onDragLeave" @drop="onDrop">
|
||||
<el-icon :size="40" class="drop-icon"><UploadFilled /></el-icon>
|
||||
<div class="drop-text">点击选择 PDF 文件</div>
|
||||
<div class="drop-hint">支持拖拽 PDF 到此处</div>
|
||||
</div>
|
||||
<div v-else class="pdf-workspace">
|
||||
<div class="pdf-canvas-area">
|
||||
<div class="canvas-container">
|
||||
<div class="canvas-placeholder">
|
||||
<el-icon :size="64" style="color: var(--text-muted)"><Document /></el-icon>
|
||||
<div class="placeholder-name">{{ getFileName() }}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="pdf-info-bar">
|
||||
<span class="pdf-filename">{{ getFileName() }}</span>
|
||||
<el-button type="danger" text size="small" @click="$emit('selectFile')"><el-icon><Delete /></el-icon></el-button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="pdf-panel">
|
||||
<div class="panel-section">
|
||||
<div class="panel-title">分割设置</div>
|
||||
<div class="param-group">
|
||||
<div class="param-label">页码范围</div>
|
||||
<el-input v-model="pageRanges" placeholder="如: 1-3,5,7-9 (留空则逐页分割)" size="small" />
|
||||
<div class="tip-text">支持格式: 1-3,5,7-9。留空将每页分割为单独文件</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="panel-section">
|
||||
<div class="panel-title">输出路径</div>
|
||||
<div class="output-row">
|
||||
<el-input :value="outputPath" :placeholder="config?.defaultOutputDir ? '默认: ' + config.defaultOutputDir : '默认: 与输入文件同目录'" readonly size="small" />
|
||||
</div>
|
||||
</div>
|
||||
<el-button class="process-btn" type="primary" size="large" :loading="processing" :disabled="!filePath" @click="onProcess">
|
||||
<el-icon v-if="!processing"><Check /></el-icon>{{ processing ? '分割中...' : '开始分割' }}
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.file-drop-zone { border: 2px dashed var(--glass-border); border-radius: var(--radius-lg); padding: 80px 40px; text-align: center; cursor: pointer; transition: all var(--transition-normal); background: var(--glass-bg); backdrop-filter: blur(var(--glass-blur)); -webkit-backdrop-filter: blur(var(--glass-blur)); max-width: 600px; margin: 40px auto; }
|
||||
.file-drop-zone:hover, .file-drop-zone.dragover { border-color: var(--accent-primary); background: rgba(59, 130, 246, 0.05); box-shadow: 0 0 20px rgba(59, 130, 246, 0.1); }
|
||||
.drop-icon { color: var(--text-muted); margin-bottom: 8px; }
|
||||
.drop-text { font-size: 15px; color: var(--text-primary); margin-bottom: 4px; }
|
||||
.drop-hint { font-size: 12px; color: var(--text-muted); }
|
||||
.pdf-workspace { display: grid; grid-template-columns: 1fr 320px; gap: 20px; height: calc(100vh - 160px); max-height: 800px; }
|
||||
.pdf-canvas-area { display: flex; flex-direction: column; background: var(--glass-bg); backdrop-filter: blur(var(--glass-blur)); -webkit-backdrop-filter: blur(var(--glass-blur)); border: 1px solid var(--glass-border); border-radius: var(--radius-lg); overflow: hidden; }
|
||||
.canvas-container { flex: 1; display: flex; align-items: center; justify-content: center; background: #1a1a25; overflow: hidden; min-height: 400px; }
|
||||
.canvas-placeholder { text-align: center; }
|
||||
.placeholder-name { margin-top: 12px; font-size: 14px; color: var(--text-secondary); max-width: 300px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
||||
.pdf-info-bar { display: flex; align-items: center; gap: 12px; padding: 10px 16px; background: var(--glass-bg); border-top: 1px solid var(--glass-border); font-size: 13px; }
|
||||
.pdf-filename { color: var(--text-primary); flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
||||
.pdf-panel { display: flex; flex-direction: column; gap: 16px; overflow-y: auto; background: var(--glass-bg); backdrop-filter: blur(var(--glass-blur)); -webkit-backdrop-filter: blur(var(--glass-blur)); border: 1px solid var(--glass-border); border-radius: var(--radius-lg); padding: 20px; }
|
||||
.panel-section { display: flex; flex-direction: column; gap: 4px; }
|
||||
.panel-title { font-size: 14px; font-weight: 600; color: var(--text-primary); margin-bottom: 8px; padding-bottom: 8px; border-bottom: 1px solid var(--glass-border); }
|
||||
.param-group { margin-bottom: 12px; }
|
||||
.param-label { font-size: 13px; font-weight: 500; color: var(--text-secondary); margin-bottom: 8px; display: flex; align-items: center; gap: 6px; }
|
||||
.tip-text { font-size: 12px; color: var(--text-muted); margin-top: 4px; }
|
||||
.output-row { display: flex; gap: 8px; }
|
||||
.output-row .el-input { flex: 1; }
|
||||
.process-btn { width: 100%; height: 48px; font-size: 15px; font-weight: 600; border-radius: var(--radius-md); background: var(--gradient-primary); border: none; transition: all var(--transition-normal); }
|
||||
.process-btn:hover:not(:disabled) { box-shadow: 0 4px 20px rgba(59, 130, 246, 0.4); transform: translateY(-1px); }
|
||||
.process-btn:active:not(:disabled) { transform: translateY(0) scale(0.98); }
|
||||
</style>
|
||||
118
frontend/src/components/pdf/PdfToImageTool.vue
Normal file
118
frontend/src/components/pdf/PdfToImageTool.vue
Normal file
@@ -0,0 +1,118 @@
|
||||
<script setup>
|
||||
import { ref } from 'vue'
|
||||
import { ElMessage } from 'element-plus'
|
||||
|
||||
const props = defineProps({
|
||||
filePath: String,
|
||||
processing: Boolean,
|
||||
resultInfo: Object,
|
||||
outputPath: String,
|
||||
config: Object,
|
||||
})
|
||||
|
||||
const emit = defineEmits(['process', 'selectFile'])
|
||||
|
||||
const imageFormat = ref('png')
|
||||
const dpi = ref(150)
|
||||
|
||||
function buildRequest() {
|
||||
return {
|
||||
inputPath: props.filePath,
|
||||
outputPath: props.outputPath,
|
||||
format: 'toImage',
|
||||
imageFormat: imageFormat.value,
|
||||
dpi: dpi.value,
|
||||
}
|
||||
}
|
||||
|
||||
function onProcess() { emit('process', buildRequest()) }
|
||||
function getFileName() { return props.filePath ? props.filePath.split(/[/\\]/).pop() : '' }
|
||||
function onDragOver(e) { e.preventDefault(); e.currentTarget.classList.add('dragover') }
|
||||
function onDragLeave(e) { e.currentTarget.classList.remove('dragover') }
|
||||
function onDrop(e) {
|
||||
e.preventDefault(); e.currentTarget.classList.remove('dragover')
|
||||
if (e.dataTransfer?.files?.length) {
|
||||
const ext = e.dataTransfer.files[0].name.split('.').pop().toLowerCase()
|
||||
if (ext !== 'pdf') { ElMessage.warning('请选择 PDF 文件'); return }
|
||||
emit('selectFile', e.dataTransfer.files[0].path)
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div v-if="!filePath" class="file-drop-zone" @click="$emit('selectFile')" @dragover="onDragOver" @dragleave="onDragLeave" @drop="onDrop">
|
||||
<el-icon :size="40" class="drop-icon"><UploadFilled /></el-icon>
|
||||
<div class="drop-text">点击选择 PDF 文件</div>
|
||||
<div class="drop-hint">支持拖拽 PDF 到此处</div>
|
||||
</div>
|
||||
<div v-else class="pdf-workspace">
|
||||
<div class="pdf-canvas-area">
|
||||
<div class="canvas-container">
|
||||
<div class="canvas-placeholder">
|
||||
<el-icon :size="64" style="color: var(--text-muted)"><Document /></el-icon>
|
||||
<div class="placeholder-name">{{ getFileName() }}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="pdf-info-bar">
|
||||
<span class="pdf-filename">{{ getFileName() }}</span>
|
||||
<el-button type="danger" text size="small" @click="$emit('selectFile')"><el-icon><Delete /></el-icon></el-button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="pdf-panel">
|
||||
<div class="panel-section">
|
||||
<div class="panel-title">转图片设置</div>
|
||||
<div class="param-group">
|
||||
<div class="param-label">输出格式</div>
|
||||
<div class="tag-group">
|
||||
<div class="tag-option" :class="{ active: imageFormat === 'png' }" @click="imageFormat = 'png'">PNG</div>
|
||||
<div class="tag-option" :class="{ active: imageFormat === 'jpeg' }" @click="imageFormat = 'jpeg'">JPEG</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="param-group">
|
||||
<div class="param-label">DPI: {{ dpi }}</div>
|
||||
<el-slider v-model="dpi" :min="72" :max="600" :step="1" />
|
||||
<div class="slider-hint"><span>72 (屏幕)</span><span>300 (打印)</span><span>600 (高清)</span></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="panel-section">
|
||||
<div class="panel-title">输出路径</div>
|
||||
<div class="output-row">
|
||||
<el-input :value="outputPath" :placeholder="config?.defaultOutputDir ? '默认: ' + config.defaultOutputDir : '默认: 与输入文件同目录'" readonly size="small" />
|
||||
</div>
|
||||
</div>
|
||||
<el-button class="process-btn" type="primary" size="large" :loading="processing" :disabled="!filePath" @click="onProcess">
|
||||
<el-icon v-if="!processing"><Check /></el-icon>{{ processing ? '转换中...' : '开始转换' }}
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.file-drop-zone { border: 2px dashed var(--glass-border); border-radius: var(--radius-lg); padding: 80px 40px; text-align: center; cursor: pointer; transition: all var(--transition-normal); background: var(--glass-bg); backdrop-filter: blur(var(--glass-blur)); -webkit-backdrop-filter: blur(var(--glass-blur)); max-width: 600px; margin: 40px auto; }
|
||||
.file-drop-zone:hover, .file-drop-zone.dragover { border-color: var(--accent-primary); background: rgba(59, 130, 246, 0.05); box-shadow: 0 0 20px rgba(59, 130, 246, 0.1); }
|
||||
.drop-icon { color: var(--text-muted); margin-bottom: 8px; }
|
||||
.drop-text { font-size: 15px; color: var(--text-primary); margin-bottom: 4px; }
|
||||
.drop-hint { font-size: 12px; color: var(--text-muted); }
|
||||
.pdf-workspace { display: grid; grid-template-columns: 1fr 320px; gap: 20px; height: calc(100vh - 160px); max-height: 800px; }
|
||||
.pdf-canvas-area { display: flex; flex-direction: column; background: var(--glass-bg); backdrop-filter: blur(var(--glass-blur)); -webkit-backdrop-filter: blur(var(--glass-blur)); border: 1px solid var(--glass-border); border-radius: var(--radius-lg); overflow: hidden; }
|
||||
.canvas-container { flex: 1; display: flex; align-items: center; justify-content: center; background: #1a1a25; overflow: hidden; min-height: 400px; }
|
||||
.canvas-placeholder { text-align: center; }
|
||||
.placeholder-name { margin-top: 12px; font-size: 14px; color: var(--text-secondary); max-width: 300px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
||||
.pdf-info-bar { display: flex; align-items: center; gap: 12px; padding: 10px 16px; background: var(--glass-bg); border-top: 1px solid var(--glass-border); font-size: 13px; }
|
||||
.pdf-filename { color: var(--text-primary); flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
||||
.pdf-panel { display: flex; flex-direction: column; gap: 16px; overflow-y: auto; background: var(--glass-bg); backdrop-filter: blur(var(--glass-blur)); -webkit-backdrop-filter: blur(var(--glass-blur)); border: 1px solid var(--glass-border); border-radius: var(--radius-lg); padding: 20px; }
|
||||
.panel-section { display: flex; flex-direction: column; gap: 4px; }
|
||||
.panel-title { font-size: 14px; font-weight: 600; color: var(--text-primary); margin-bottom: 8px; padding-bottom: 8px; border-bottom: 1px solid var(--glass-border); }
|
||||
.param-group { margin-bottom: 12px; }
|
||||
.param-label { font-size: 13px; font-weight: 500; color: var(--text-secondary); margin-bottom: 8px; display: flex; align-items: center; gap: 6px; }
|
||||
.tag-group { display: flex; gap: 6px; flex-wrap: wrap; }
|
||||
.tag-option { display: flex; align-items: center; gap: 4px; padding: 7px 14px; border-radius: var(--radius-sm); font-size: 13px; font-weight: 500; cursor: pointer; background: var(--bg-surface); color: var(--text-secondary); border: 1px solid var(--glass-border); transition: all var(--transition-normal); user-select: none; }
|
||||
.tag-option:hover { color: var(--text-primary); border-color: rgba(255, 255, 255, 0.15); background: rgba(255, 255, 255, 0.05); }
|
||||
.tag-option.active { background: var(--accent-primary); color: #ffffff; border-color: var(--accent-primary); box-shadow: 0 2px 8px rgba(59, 130, 246, 0.3); }
|
||||
.slider-hint { display: flex; justify-content: space-between; font-size: 11px; color: var(--text-muted); margin-top: 4px; }
|
||||
.output-row { display: flex; gap: 8px; }
|
||||
.output-row .el-input { flex: 1; }
|
||||
.process-btn { width: 100%; height: 48px; font-size: 15px; font-weight: 600; border-radius: var(--radius-md); background: var(--gradient-primary); border: none; transition: all var(--transition-normal); }
|
||||
.process-btn:hover:not(:disabled) { box-shadow: 0 4px 20px rgba(59, 130, 246, 0.4); transform: translateY(-1px); }
|
||||
.process-btn:active:not(:disabled) { transform: translateY(0) scale(0.98); }
|
||||
</style>
|
||||
97
frontend/src/components/pdf/PdfWatermarkTool.vue
Normal file
97
frontend/src/components/pdf/PdfWatermarkTool.vue
Normal file
@@ -0,0 +1,97 @@
|
||||
<script setup>
|
||||
import { ref } from 'vue'
|
||||
import { ElMessage } from 'element-plus'
|
||||
|
||||
const props = defineProps({
|
||||
filePath: String,
|
||||
processing: Boolean,
|
||||
resultInfo: Object,
|
||||
outputPath: String,
|
||||
config: Object,
|
||||
})
|
||||
|
||||
const emit = defineEmits(['process', 'selectFile'])
|
||||
|
||||
const watermarkText = ref('')
|
||||
|
||||
function buildRequest() {
|
||||
return { inputPath: props.filePath, outputPath: props.outputPath, format: 'watermark', text: watermarkText.value }
|
||||
}
|
||||
function onProcess() { emit('process', buildRequest()) }
|
||||
function getFileName() { return props.filePath ? props.filePath.split(/[/\\]/).pop() : '' }
|
||||
function onDragOver(e) { e.preventDefault(); e.currentTarget.classList.add('dragover') }
|
||||
function onDragLeave(e) { e.currentTarget.classList.remove('dragover') }
|
||||
function onDrop(e) {
|
||||
e.preventDefault(); e.currentTarget.classList.remove('dragover')
|
||||
if (e.dataTransfer?.files?.length) {
|
||||
const ext = e.dataTransfer.files[0].name.split('.').pop().toLowerCase()
|
||||
if (ext !== 'pdf') { ElMessage.warning('请选择 PDF 文件'); return }
|
||||
emit('selectFile', e.dataTransfer.files[0].path)
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div v-if="!filePath" class="file-drop-zone" @click="$emit('selectFile')" @dragover="onDragOver" @dragleave="onDragLeave" @drop="onDrop">
|
||||
<el-icon :size="40" class="drop-icon"><UploadFilled /></el-icon>
|
||||
<div class="drop-text">点击选择 PDF 文件</div>
|
||||
<div class="drop-hint">支持拖拽 PDF 到此处</div>
|
||||
</div>
|
||||
<div v-else class="pdf-workspace">
|
||||
<div class="pdf-canvas-area">
|
||||
<div class="canvas-container">
|
||||
<div class="canvas-placeholder">
|
||||
<el-icon :size="64" style="color: var(--text-muted)"><Document /></el-icon>
|
||||
<div class="placeholder-name">{{ getFileName() }}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="pdf-info-bar">
|
||||
<span class="pdf-filename">{{ getFileName() }}</span>
|
||||
<el-button type="danger" text size="small" @click="$emit('selectFile')"><el-icon><Delete /></el-icon></el-button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="pdf-panel">
|
||||
<div class="panel-section">
|
||||
<div class="panel-title">水印设置</div>
|
||||
<div class="param-group">
|
||||
<div class="param-label">水印文字</div>
|
||||
<el-input v-model="watermarkText" placeholder="请输入水印文字" size="small" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="panel-section">
|
||||
<div class="panel-title">输出路径</div>
|
||||
<div class="output-row">
|
||||
<el-input :value="outputPath" :placeholder="config?.defaultOutputDir ? '默认: ' + config.defaultOutputDir : '默认: 与输入文件同目录'" readonly size="small" />
|
||||
</div>
|
||||
</div>
|
||||
<el-button class="process-btn" type="primary" size="large" :loading="processing" :disabled="!filePath || !watermarkText" @click="onProcess">
|
||||
<el-icon v-if="!processing"><Check /></el-icon>{{ processing ? '处理中...' : '添加水印' }}
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.file-drop-zone { border: 2px dashed var(--glass-border); border-radius: var(--radius-lg); padding: 80px 40px; text-align: center; cursor: pointer; transition: all var(--transition-normal); background: var(--glass-bg); backdrop-filter: blur(var(--glass-blur)); -webkit-backdrop-filter: blur(var(--glass-blur)); max-width: 600px; margin: 40px auto; }
|
||||
.file-drop-zone:hover, .file-drop-zone.dragover { border-color: var(--accent-primary); background: rgba(59, 130, 246, 0.05); box-shadow: 0 0 20px rgba(59, 130, 246, 0.1); }
|
||||
.drop-icon { color: var(--text-muted); margin-bottom: 8px; }
|
||||
.drop-text { font-size: 15px; color: var(--text-primary); margin-bottom: 4px; }
|
||||
.drop-hint { font-size: 12px; color: var(--text-muted); }
|
||||
.pdf-workspace { display: grid; grid-template-columns: 1fr 320px; gap: 20px; height: calc(100vh - 160px); max-height: 800px; }
|
||||
.pdf-canvas-area { display: flex; flex-direction: column; background: var(--glass-bg); backdrop-filter: blur(var(--glass-blur)); -webkit-backdrop-filter: blur(var(--glass-blur)); border: 1px solid var(--glass-border); border-radius: var(--radius-lg); overflow: hidden; }
|
||||
.canvas-container { flex: 1; display: flex; align-items: center; justify-content: center; background: #1a1a25; overflow: hidden; min-height: 400px; }
|
||||
.canvas-placeholder { text-align: center; }
|
||||
.placeholder-name { margin-top: 12px; font-size: 14px; color: var(--text-secondary); max-width: 300px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
||||
.pdf-info-bar { display: flex; align-items: center; gap: 12px; padding: 10px 16px; background: var(--glass-bg); border-top: 1px solid var(--glass-border); font-size: 13px; }
|
||||
.pdf-filename { color: var(--text-primary); flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
||||
.pdf-panel { display: flex; flex-direction: column; gap: 16px; overflow-y: auto; background: var(--glass-bg); backdrop-filter: blur(var(--glass-blur)); -webkit-backdrop-filter: blur(var(--glass-blur)); border: 1px solid var(--glass-border); border-radius: var(--radius-lg); padding: 20px; }
|
||||
.panel-section { display: flex; flex-direction: column; gap: 4px; }
|
||||
.panel-title { font-size: 14px; font-weight: 600; color: var(--text-primary); margin-bottom: 8px; padding-bottom: 8px; border-bottom: 1px solid var(--glass-border); }
|
||||
.param-group { margin-bottom: 12px; }
|
||||
.param-label { font-size: 13px; font-weight: 500; color: var(--text-secondary); margin-bottom: 8px; display: flex; align-items: center; gap: 6px; }
|
||||
.output-row { display: flex; gap: 8px; }
|
||||
.output-row .el-input { flex: 1; }
|
||||
.process-btn { width: 100%; height: 48px; font-size: 15px; font-weight: 600; border-radius: var(--radius-md); background: var(--gradient-primary); border: none; transition: all var(--transition-normal); }
|
||||
.process-btn:hover:not(:disabled) { box-shadow: 0 4px 20px rgba(59, 130, 246, 0.4); transform: translateY(-1px); }
|
||||
.process-btn:active:not(:disabled) { transform: translateY(0) scale(0.98); }
|
||||
</style>
|
||||
12
frontend/wailsjs/go/main/FileHandler.d.ts
vendored
12
frontend/wailsjs/go/main/FileHandler.d.ts
vendored
@@ -4,18 +4,30 @@ import {main} from '../models';
|
||||
|
||||
export function AddRecentUse(arg1:main.RecentUse):Promise<void>;
|
||||
|
||||
export function ExtractPDFText(arg1:string):Promise<string>;
|
||||
|
||||
export function GetCompareImages(arg1:string,arg2:string):Promise<Record<string, string>>;
|
||||
|
||||
export function GetConfig():Promise<main.AppConfig>;
|
||||
|
||||
export function GetExeDir():Promise<string>;
|
||||
|
||||
export function GetFileInfo(arg1:string):Promise<Record<string, any>>;
|
||||
|
||||
export function GetImageBase64(arg1:string):Promise<string>;
|
||||
|
||||
export function GetLogDates():Promise<Array<string>>;
|
||||
|
||||
export function GetLogs(arg1:string):Promise<string>;
|
||||
|
||||
export function GetRecentUses():Promise<Array<main.RecentUse>>;
|
||||
|
||||
export function GetShortcuts():Promise<Array<main.Shortcut>>;
|
||||
|
||||
export function LogError(arg1:string):Promise<void>;
|
||||
|
||||
export function LogInfo(arg1:string):Promise<void>;
|
||||
|
||||
export function OpenFileDialog(arg1:string,arg2:Array<string>):Promise<string>;
|
||||
|
||||
export function OpenFolder(arg1:string):Promise<void>;
|
||||
|
||||
@@ -6,6 +6,10 @@ export function AddRecentUse(arg1) {
|
||||
return window['go']['main']['FileHandler']['AddRecentUse'](arg1);
|
||||
}
|
||||
|
||||
export function ExtractPDFText(arg1) {
|
||||
return window['go']['main']['FileHandler']['ExtractPDFText'](arg1);
|
||||
}
|
||||
|
||||
export function GetCompareImages(arg1, arg2) {
|
||||
return window['go']['main']['FileHandler']['GetCompareImages'](arg1, arg2);
|
||||
}
|
||||
@@ -14,6 +18,10 @@ export function GetConfig() {
|
||||
return window['go']['main']['FileHandler']['GetConfig']();
|
||||
}
|
||||
|
||||
export function GetExeDir() {
|
||||
return window['go']['main']['FileHandler']['GetExeDir']();
|
||||
}
|
||||
|
||||
export function GetFileInfo(arg1) {
|
||||
return window['go']['main']['FileHandler']['GetFileInfo'](arg1);
|
||||
}
|
||||
@@ -22,6 +30,14 @@ export function GetImageBase64(arg1) {
|
||||
return window['go']['main']['FileHandler']['GetImageBase64'](arg1);
|
||||
}
|
||||
|
||||
export function GetLogDates() {
|
||||
return window['go']['main']['FileHandler']['GetLogDates']();
|
||||
}
|
||||
|
||||
export function GetLogs(arg1) {
|
||||
return window['go']['main']['FileHandler']['GetLogs'](arg1);
|
||||
}
|
||||
|
||||
export function GetRecentUses() {
|
||||
return window['go']['main']['FileHandler']['GetRecentUses']();
|
||||
}
|
||||
@@ -30,6 +46,14 @@ export function GetShortcuts() {
|
||||
return window['go']['main']['FileHandler']['GetShortcuts']();
|
||||
}
|
||||
|
||||
export function LogError(arg1) {
|
||||
return window['go']['main']['FileHandler']['LogError'](arg1);
|
||||
}
|
||||
|
||||
export function LogInfo(arg1) {
|
||||
return window['go']['main']['FileHandler']['LogInfo'](arg1);
|
||||
}
|
||||
|
||||
export function OpenFileDialog(arg1, arg2) {
|
||||
return window['go']['main']['FileHandler']['OpenFileDialog'](arg1, arg2);
|
||||
}
|
||||
|
||||
@@ -34,8 +34,10 @@ export namespace main {
|
||||
}
|
||||
export class ProcessRequest {
|
||||
inputPath: string;
|
||||
inputPaths?: string[];
|
||||
outputPath?: string;
|
||||
format?: string;
|
||||
outputFormat?: string;
|
||||
quality?: string;
|
||||
qualityInt?: number;
|
||||
width?: number;
|
||||
@@ -55,6 +57,10 @@ export namespace main {
|
||||
flipH?: boolean;
|
||||
flipV?: boolean;
|
||||
maintainRatio?: boolean;
|
||||
pageRanges?: string;
|
||||
dpi?: number;
|
||||
imageFormat?: string;
|
||||
optimizeLevel?: string;
|
||||
cropX?: number;
|
||||
cropY?: number;
|
||||
|
||||
@@ -65,8 +71,10 @@ export namespace main {
|
||||
constructor(source: any = {}) {
|
||||
if ('string' === typeof source) source = JSON.parse(source);
|
||||
this.inputPath = source["inputPath"];
|
||||
this.inputPaths = source["inputPaths"];
|
||||
this.outputPath = source["outputPath"];
|
||||
this.format = source["format"];
|
||||
this.outputFormat = source["outputFormat"];
|
||||
this.quality = source["quality"];
|
||||
this.qualityInt = source["qualityInt"];
|
||||
this.width = source["width"];
|
||||
@@ -86,6 +94,10 @@ export namespace main {
|
||||
this.flipH = source["flipH"];
|
||||
this.flipV = source["flipV"];
|
||||
this.maintainRatio = source["maintainRatio"];
|
||||
this.pageRanges = source["pageRanges"];
|
||||
this.dpi = source["dpi"];
|
||||
this.imageFormat = source["imageFormat"];
|
||||
this.optimizeLevel = source["optimizeLevel"];
|
||||
this.cropX = source["cropX"];
|
||||
this.cropY = source["cropY"];
|
||||
}
|
||||
|
||||
10
go.mod
10
go.mod
@@ -4,6 +4,8 @@ go 1.25.0
|
||||
|
||||
require (
|
||||
github.com/disintegration/imaging v1.6.2
|
||||
github.com/gen2brain/go-fitz v1.24.15
|
||||
github.com/pdfcpu/pdfcpu v0.13.0
|
||||
github.com/signintech/gopdf v0.36.1
|
||||
github.com/wailsapp/wails/v2 v2.12.0
|
||||
github.com/xuri/excelize/v2 v2.10.1
|
||||
@@ -13,11 +15,17 @@ require (
|
||||
require (
|
||||
git.sr.ht/~jackmordaunt/go-toast/v2 v2.0.3 // indirect
|
||||
github.com/bep/debounce v1.2.1 // indirect
|
||||
github.com/clipperhouse/uax29/v2 v2.7.0 // indirect
|
||||
github.com/ebitengine/purego v0.8.4 // indirect
|
||||
github.com/go-ole/go-ole v1.3.0 // indirect
|
||||
github.com/godbus/dbus/v5 v5.1.0 // indirect
|
||||
github.com/google/uuid v1.6.0 // indirect
|
||||
github.com/gorilla/websocket v1.5.3 // indirect
|
||||
github.com/hhrutter/lzw v1.0.0 // indirect
|
||||
github.com/hhrutter/pkcs7 v0.2.2 // indirect
|
||||
github.com/hhrutter/tiff v1.0.3 // indirect
|
||||
github.com/jchv/go-winloader v0.0.0-20210711035445-715c2860da7e // indirect
|
||||
github.com/jupiterrider/ffi v0.5.0 // indirect
|
||||
github.com/labstack/echo/v4 v4.13.3 // indirect
|
||||
github.com/labstack/gommon v0.4.2 // indirect
|
||||
github.com/leaanthony/go-ansi-parser v1.6.1 // indirect
|
||||
@@ -26,6 +34,7 @@ require (
|
||||
github.com/leaanthony/u v1.1.1 // indirect
|
||||
github.com/mattn/go-colorable v0.1.13 // indirect
|
||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||
github.com/mattn/go-runewidth v0.0.24 // indirect
|
||||
github.com/phpdave11/gofpdi v1.0.14-0.20211212211723-1f10f9844311 // indirect
|
||||
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c // indirect
|
||||
github.com/pkg/errors v0.9.1 // indirect
|
||||
@@ -45,6 +54,7 @@ require (
|
||||
golang.org/x/net v0.54.0 // indirect
|
||||
golang.org/x/sys v0.45.0 // indirect
|
||||
golang.org/x/text v0.37.0 // indirect
|
||||
gopkg.in/yaml.v2 v2.4.0 // indirect
|
||||
)
|
||||
|
||||
// replace github.com/wailsapp/wails/v2 v2.12.0 => C:\Users\admin\go\pkg\mod
|
||||
|
||||
27
go.sum
27
go.sum
@@ -2,10 +2,16 @@ git.sr.ht/~jackmordaunt/go-toast/v2 v2.0.3 h1:N3IGoHHp9pb6mj1cbXbuaSXV/UMKwmbKLf
|
||||
git.sr.ht/~jackmordaunt/go-toast/v2 v2.0.3/go.mod h1:QtOLZGz8olr4qH2vWK0QH0w0O4T9fEIjMuWpKUsH7nc=
|
||||
github.com/bep/debounce v1.2.1 h1:v67fRdBA9UQu2NhLFXrSg0Brw7CexQekrBwDMM8bzeY=
|
||||
github.com/bep/debounce v1.2.1/go.mod h1:H8yggRPQKLUhUoqrJC1bO2xNya7vanpDl7xR3ISbCJ0=
|
||||
github.com/clipperhouse/uax29/v2 v2.7.0 h1:+gs4oBZ2gPfVrKPthwbMzWZDaAFPGYK72F0NJv2v7Vk=
|
||||
github.com/clipperhouse/uax29/v2 v2.7.0/go.mod h1:EFJ2TJMRUaplDxHKj1qAEhCtQPW2tJSwu5BF98AuoVM=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/disintegration/imaging v1.6.2 h1:w1LecBlG2Lnp8B3jk5zSuNqd7b4DXhcjwek1ei82L+c=
|
||||
github.com/disintegration/imaging v1.6.2/go.mod h1:44/5580QXChDfwIclfc/PCwrr44amcmDAg8hxG0Ewe4=
|
||||
github.com/ebitengine/purego v0.8.4 h1:CF7LEKg5FFOsASUj0+QwaXf8Ht6TlFxg09+S9wz0omw=
|
||||
github.com/ebitengine/purego v0.8.4/go.mod h1:iIjxzd6CiRiOG0UyXP+V1+jWqUXVjPKLAI0mRfJZTmQ=
|
||||
github.com/gen2brain/go-fitz v1.24.15 h1:sJNB1MOWkqnzzENPHggFpgxTwW0+S5WF/rM5wUBpJWo=
|
||||
github.com/gen2brain/go-fitz v1.24.15/go.mod h1:SftkiVbTHqF141DuiLwBBM65zP7ig6AVDQpf2WlHamo=
|
||||
github.com/go-ole/go-ole v1.3.0 h1:Dt6ye7+vXGIKZ7Xtk4s6/xVdGDQynvom7xCFEdWr6uE=
|
||||
github.com/go-ole/go-ole v1.3.0/go.mod h1:5LS6F96DhAwUc7C+1HLexzMXY1xGRSryjyPPKW6zv78=
|
||||
github.com/godbus/dbus/v5 v5.1.0 h1:4KLkAxT3aOY8Li4FRJe/KvhoNFFxo0m6fNuFUO8QJUk=
|
||||
@@ -14,8 +20,18 @@ github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg=
|
||||
github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
|
||||
github.com/hhrutter/lzw v1.0.0 h1:laL89Llp86W3rRs83LvKbwYRx6INE8gDn0XNb1oXtm0=
|
||||
github.com/hhrutter/lzw v1.0.0/go.mod h1:2HC6DJSn/n6iAZfgM3Pg+cP1KxeWc3ezG8bBqW5+WEo=
|
||||
github.com/hhrutter/pkcs7 v0.2.2 h1:xMoifoVWah1LNym3C0pomEiLmyJyVIBXt/8oTPyPz+8=
|
||||
github.com/hhrutter/pkcs7 v0.2.2/go.mod h1:aEzKz0+ZAlz7YaEMY47jDHL14hVWD6iXt0AgqgAvWgE=
|
||||
github.com/hhrutter/tiff v1.0.3 h1:POV5xITOE1Lt5FvP24ylft0LyCmHmc8GkJ1SVlvUyk0=
|
||||
github.com/hhrutter/tiff v1.0.3/go.mod h1:zZDLVY4cp9za2FLrryAaGszwWYAUM6DrRiBR0l//mxA=
|
||||
github.com/jchv/go-winloader v0.0.0-20210711035445-715c2860da7e h1:Q3+PugElBCf4PFpxhErSzU3/PY5sFL5Z6rfv4AbGAck=
|
||||
github.com/jchv/go-winloader v0.0.0-20210711035445-715c2860da7e/go.mod h1:alcuEEnZsY1WQsagKhZDsoPCRoOijYqhZvPwLG0kzVs=
|
||||
github.com/jupiterrider/ffi v0.5.0 h1:j2nSgpabbV1JOwgP4Kn449sJUHq3cVLAZVBoOYn44V8=
|
||||
github.com/jupiterrider/ffi v0.5.0/go.mod h1:x7xdNKo8h0AmLuXfswDUBxUsd2OqUP4ekC8sCnsmbvo=
|
||||
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
||||
github.com/labstack/echo/v4 v4.13.3 h1:pwhpCPrTl5qry5HRdM5FwdXnhXSLSY+WE+YQSeCaafY=
|
||||
github.com/labstack/echo/v4 v4.13.3/go.mod h1:o90YNEeQWjDozo584l7AwhJMHN0bOC4tAfg+Xox9q5g=
|
||||
github.com/labstack/gommon v0.4.2 h1:F8qTUNXgG1+6WQmqoUWnz8WiEU60mXVVw0P4ht1WRA0=
|
||||
@@ -38,6 +54,12 @@ github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovk
|
||||
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
|
||||
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
||||
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||
github.com/mattn/go-runewidth v0.0.24 h1:cpokDiIn0MGnhdHwuWnJBITySJ20QyNGnY2kR/ay2DU=
|
||||
github.com/mattn/go-runewidth v0.0.24/go.mod h1:XBkDxAl56ILZc9knddidhrOlY5R/pDhgLpndooCuJAs=
|
||||
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e h1:fD57ERR4JtEqsWbfPhv4DMiApHyliiK5xCTNVSPiaAs=
|
||||
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno=
|
||||
github.com/pdfcpu/pdfcpu v0.13.0 h1:7maI7K0w4pJsgX9u7eeCsK4+An/+xEVkJwAwyd7/n3M=
|
||||
github.com/pdfcpu/pdfcpu v0.13.0/go.mod h1:Pz8elxcY3MHc3W65HeeDbuSBvsq+OK+enMVdBsvKCj4=
|
||||
github.com/phpdave11/gofpdi v1.0.14-0.20211212211723-1f10f9844311 h1:zyWXQ6vu27ETMpYsEMAsisQ+GqJ4e1TPvSNfdOPF0no=
|
||||
github.com/phpdave11/gofpdi v1.0.14-0.20211212211723-1f10f9844311/go.mod h1:vBmVV0Do6hSBHC8uKUQ71JGW+ZGQq74llk/7bXwjDoI=
|
||||
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c h1:+mdjkGKdHQG3305AYmdv1U2eRNDiU2ErMBj1gwrq8eQ=
|
||||
@@ -102,5 +124,10 @@ golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.37.0 h1:Cqjiwd9eSg8e0QAkyCaQTNHFIIzWtidPahFWR83rTrc=
|
||||
golang.org/x/text v0.37.0/go.mod h1:a5sjxXGs9hsn/AJVwuElvCAo9v8QYLzvavO5z2PiM38=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f h1:BLraFXnmrev5lT+xlilqcH8XK9/i0At2xKjWk4p6zsU=
|
||||
gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
|
||||
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
|
||||
202
handlers.go
202
handlers.go
@@ -8,6 +8,7 @@ import (
|
||||
"path/filepath"
|
||||
stdruntime "runtime"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
wailsRuntime "github.com/wailsapp/wails/v2/pkg/runtime"
|
||||
|
||||
@@ -45,8 +46,10 @@ type FileResult struct {
|
||||
|
||||
type ProcessRequest struct {
|
||||
InputPath string `json:"inputPath"`
|
||||
InputPaths []string `json:"inputPaths,omitempty"`
|
||||
OutputPath string `json:"outputPath,omitempty"`
|
||||
Format string `json:"format,omitempty"`
|
||||
OutputFormat string `json:"outputFormat,omitempty"`
|
||||
Quality string `json:"quality,omitempty"`
|
||||
QualityInt int `json:"qualityInt,omitempty"`
|
||||
Width int `json:"width,omitempty"`
|
||||
@@ -66,6 +69,10 @@ type ProcessRequest struct {
|
||||
FlipH bool `json:"flipH,omitempty"`
|
||||
FlipV bool `json:"flipV,omitempty"`
|
||||
MaintainRatio *bool `json:"maintainRatio,omitempty"`
|
||||
PageRanges string `json:"pageRanges,omitempty"`
|
||||
DPI float64 `json:"dpi,omitempty"`
|
||||
ImageFormat string `json:"imageFormat,omitempty"`
|
||||
OptimizeLevel string `json:"optimizeLevel,omitempty"`
|
||||
CropX int `json:"cropX,omitempty"`
|
||||
CropY int `json:"cropY,omitempty"`
|
||||
}
|
||||
@@ -299,6 +306,8 @@ func (h *FileHandler) ProcessFile(req ProcessRequest) FileResult {
|
||||
return h.processExcel(req)
|
||||
case ".jpg", ".jpeg", ".png", ".gif", ".bmp", ".tiff", ".webp", ".ico":
|
||||
return h.processImage(req)
|
||||
case ".txt", ".md", ".html":
|
||||
return h.processTextToPDF(req)
|
||||
default:
|
||||
return FileResult{Success: false, Message: fmt.Sprintf("不支持的文件格式: %s", ext)}
|
||||
}
|
||||
@@ -329,15 +338,108 @@ func (h *FileHandler) processPDF(req ProcessRequest) FileResult {
|
||||
|
||||
switch req.Format {
|
||||
case "compress":
|
||||
err = h.pdfService.CompressPDF(req.InputPath, outputPath, req.Quality)
|
||||
err = h.pdfService.OptimizePDF(req.InputPath, outputPath, req.OptimizeLevel)
|
||||
case "split":
|
||||
_, err = h.pdfService.SplitPDF(req.InputPath, filepath.Dir(outputPath), "")
|
||||
outputDir := filepath.Dir(outputPath)
|
||||
_, err = h.pdfService.SplitPDF(req.InputPath, outputDir, req.PageRanges)
|
||||
case "rotate":
|
||||
err = h.pdfService.RotatePDF(req.InputPath, outputPath, int(req.Angle))
|
||||
case "watermark":
|
||||
err = h.pdfService.AddWatermark(req.InputPath, outputPath, req.Text)
|
||||
case "merge":
|
||||
err = h.pdfService.MergePDFs(req.InputPaths, outputPath)
|
||||
case "toImage":
|
||||
outputDir := filepath.Dir(outputPath)
|
||||
results, imgErr := h.pdfService.PDFToImages(req.InputPath, outputDir, req.ImageFormat, req.DPI)
|
||||
if imgErr != nil {
|
||||
err = imgErr
|
||||
} else if len(results) > 0 {
|
||||
return FileResult{Success: true, Message: fmt.Sprintf("已生成 %d 张图片", len(results)), Path: results[0], Size: 0}
|
||||
}
|
||||
case "toText":
|
||||
text, txtErr := h.pdfService.ExtractText(req.InputPath)
|
||||
if txtErr != nil {
|
||||
err = txtErr
|
||||
} else {
|
||||
txtPath := services.ChangeExtension(outputPath, ".txt")
|
||||
if writeErr := os.WriteFile(txtPath, []byte(text), 0644); writeErr != nil {
|
||||
err = writeErr
|
||||
} else {
|
||||
return h.getFileResult(req.InputPath, txtPath)
|
||||
}
|
||||
}
|
||||
case "toWord":
|
||||
text, txtErr := h.pdfService.ExtractText(req.InputPath)
|
||||
if txtErr != nil {
|
||||
err = txtErr
|
||||
} else {
|
||||
txtPath := services.ChangeExtension(outputPath, ".txt")
|
||||
if writeErr := os.WriteFile(txtPath, []byte(text), 0644); writeErr != nil {
|
||||
err = writeErr
|
||||
} else {
|
||||
return h.getFileResult(req.InputPath, txtPath)
|
||||
}
|
||||
}
|
||||
case "toExcel":
|
||||
text, txtErr := h.pdfService.ExtractText(req.InputPath)
|
||||
if txtErr != nil {
|
||||
err = txtErr
|
||||
} else {
|
||||
csvPath := services.ChangeExtension(outputPath, ".csv")
|
||||
lines := strings.Split(text, "\n")
|
||||
var csvContent strings.Builder
|
||||
for _, line := range lines {
|
||||
line = strings.TrimSpace(line)
|
||||
if line != "" {
|
||||
if strings.ContainsAny(line, ",\"") {
|
||||
csvContent.WriteString("\"" + strings.ReplaceAll(line, "\"", "\"\"") + "\"\n")
|
||||
} else {
|
||||
csvContent.WriteString(line + "\n")
|
||||
}
|
||||
}
|
||||
}
|
||||
if writeErr := os.WriteFile(csvPath, []byte(csvContent.String()), 0644); writeErr != nil {
|
||||
err = writeErr
|
||||
} else {
|
||||
return h.getFileResult(req.InputPath, csvPath)
|
||||
}
|
||||
}
|
||||
case "toHtml":
|
||||
text, txtErr := h.pdfService.ExtractText(req.InputPath)
|
||||
if txtErr != nil {
|
||||
err = txtErr
|
||||
} else {
|
||||
htmlPath := services.ChangeExtension(outputPath, ".html")
|
||||
lines := strings.Split(text, "\n")
|
||||
var html strings.Builder
|
||||
html.WriteString("<!DOCTYPE html>\n<html>\n<head>\n<meta charset=\"utf-8\">\n<title>PDF Text</title>\n<style>body{font-family:sans-serif;max-width:800px;margin:0 auto;padding:20px;line-height:1.6}</style>\n</head>\n<body>\n")
|
||||
for _, line := range lines {
|
||||
line = strings.TrimSpace(line)
|
||||
if line != "" {
|
||||
html.WriteString("<p>" + line + "</p>\n")
|
||||
}
|
||||
}
|
||||
html.WriteString("</body>\n</html>")
|
||||
if writeErr := os.WriteFile(htmlPath, []byte(html.String()), 0644); writeErr != nil {
|
||||
err = writeErr
|
||||
} else {
|
||||
return h.getFileResult(req.InputPath, htmlPath)
|
||||
}
|
||||
}
|
||||
case "toMarkdown":
|
||||
text, txtErr := h.pdfService.ExtractText(req.InputPath)
|
||||
if txtErr != nil {
|
||||
err = txtErr
|
||||
} else {
|
||||
mdPath := services.ChangeExtension(outputPath, ".md")
|
||||
if writeErr := os.WriteFile(mdPath, []byte(text), 0644); writeErr != nil {
|
||||
err = writeErr
|
||||
} else {
|
||||
return h.getFileResult(req.InputPath, mdPath)
|
||||
}
|
||||
}
|
||||
default:
|
||||
err = h.pdfService.CompressPDF(req.InputPath, outputPath, "medium")
|
||||
err = h.pdfService.OptimizePDF(req.InputPath, outputPath, "medium")
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
@@ -378,6 +480,26 @@ func (h *FileHandler) processExcel(req ProcessRequest) FileResult {
|
||||
return h.getFileResult(req.InputPath, outputPath)
|
||||
}
|
||||
|
||||
func (h *FileHandler) processTextToPDF(req ProcessRequest) FileResult {
|
||||
outputPath := h.resolveOutputPath(req.InputPath, req.OutputPath, "", ".pdf")
|
||||
|
||||
ext := strings.ToLower(filepath.Ext(req.InputPath))
|
||||
var err error
|
||||
switch ext {
|
||||
case ".md":
|
||||
err = h.pdfService.MarkdownToPDF(req.InputPath, outputPath)
|
||||
case ".html":
|
||||
err = h.pdfService.HTMLToPDF(req.InputPath, outputPath)
|
||||
default:
|
||||
err = h.pdfService.TextToPDF(req.InputPath, outputPath)
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return FileResult{Success: false, Message: err.Error()}
|
||||
}
|
||||
return h.getFileResult(req.InputPath, outputPath)
|
||||
}
|
||||
|
||||
func (h *FileHandler) processImage(req ProcessRequest) FileResult {
|
||||
isImageOp := req.Format == "compress" || req.Format == "resize" ||
|
||||
req.Format == "rotate" || req.Format == "grayscale" || req.Format == "brightness" ||
|
||||
@@ -482,12 +604,17 @@ func (h *FileHandler) processImage(req ProcessRequest) FileResult {
|
||||
Type: "rectangle", X: req.CropX, Y: req.CropY, Width: req.Width, Height: req.Height,
|
||||
})
|
||||
}
|
||||
case "toPdf":
|
||||
if outputPath == "" {
|
||||
outputPath = services.ChangeExtension(req.InputPath, ".pdf")
|
||||
}
|
||||
err = h.imageService.ImageToPDF(req.InputPath, outputPath)
|
||||
default:
|
||||
quality := 85
|
||||
if req.QualityInt > 0 {
|
||||
quality = req.QualityInt
|
||||
}
|
||||
err = h.imageService.ConvertFormat(req.InputPath, outputPath, req.Format, quality)
|
||||
err = h.imageService.ConvertFormat(req.InputPath, outputPath, h.getConvertFormat(req), quality)
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
@@ -502,11 +629,13 @@ func (h *FileHandler) processImage(req ProcessRequest) FileResult {
|
||||
}
|
||||
|
||||
func (h *FileHandler) getConvertFormat(req ProcessRequest) string {
|
||||
format := req.Format
|
||||
if format == "convert" {
|
||||
if req.OutputFormat != "" {
|
||||
return req.OutputFormat
|
||||
}
|
||||
if req.Format == "convert" {
|
||||
return "png"
|
||||
}
|
||||
return format
|
||||
return req.Format
|
||||
}
|
||||
|
||||
func (h *FileHandler) getFileResult(inputPath, outputPath string) FileResult {
|
||||
@@ -562,6 +691,65 @@ func (h *FileHandler) GetFileInfo(path string) map[string]interface{} {
|
||||
return result
|
||||
}
|
||||
|
||||
func (h *FileHandler) ExtractPDFText(path string) (string, error) {
|
||||
return h.pdfService.ExtractText(path)
|
||||
}
|
||||
|
||||
func (h *FileHandler) LogError(msg string) {
|
||||
h.writeLog("ERROR", msg)
|
||||
}
|
||||
|
||||
func (h *FileHandler) LogInfo(msg string) {
|
||||
h.writeLog("INFO", msg)
|
||||
}
|
||||
|
||||
func (h *FileHandler) writeLog(level, msg string) {
|
||||
exe, _ := os.Executable()
|
||||
logDir := filepath.Join(filepath.Dir(exe), "logs")
|
||||
if err := os.MkdirAll(logDir, 0755); err != nil {
|
||||
return
|
||||
}
|
||||
logFile := filepath.Join(logDir, time.Now().Format("2006-01-02")+".log")
|
||||
f, err := os.OpenFile(logFile, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
defer f.Close()
|
||||
fmt.Fprintf(f, "[%s] [%s] %s\n", time.Now().Format("15:04:05"), level, msg)
|
||||
}
|
||||
|
||||
func (h *FileHandler) GetLogDates() []string {
|
||||
exe, _ := os.Executable()
|
||||
logDir := filepath.Join(filepath.Dir(exe), "logs")
|
||||
entries, err := os.ReadDir(logDir)
|
||||
if err != nil {
|
||||
return []string{}
|
||||
}
|
||||
var dates []string
|
||||
for _, e := range entries {
|
||||
if !e.IsDir() && strings.HasSuffix(e.Name(), ".log") {
|
||||
dates = append(dates, strings.TrimSuffix(e.Name(), ".log"))
|
||||
}
|
||||
}
|
||||
return dates
|
||||
}
|
||||
|
||||
func (h *FileHandler) GetLogs(date string) string {
|
||||
exe, _ := os.Executable()
|
||||
logDir := filepath.Join(filepath.Dir(exe), "logs")
|
||||
logFile := filepath.Join(logDir, date+".log")
|
||||
data, err := os.ReadFile(logFile)
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
return string(data)
|
||||
}
|
||||
|
||||
func (h *FileHandler) GetExeDir() string {
|
||||
exe, _ := os.Executable()
|
||||
return filepath.Dir(exe)
|
||||
}
|
||||
|
||||
func getConfigPath() string {
|
||||
home, _ := os.UserHomeDir()
|
||||
return filepath.Join(home, ".xk-config.json")
|
||||
|
||||
28
main.go
28
main.go
@@ -3,6 +3,8 @@ package main
|
||||
import (
|
||||
"context"
|
||||
"embed"
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/wailsapp/wails/v2"
|
||||
"github.com/wailsapp/wails/v2/pkg/options"
|
||||
@@ -12,12 +14,36 @@ import (
|
||||
//go:embed all:frontend/dist
|
||||
var assets embed.FS
|
||||
|
||||
//go:embed public/MuPDFLib.dll
|
||||
var mupdfDLL []byte
|
||||
|
||||
func init() {
|
||||
exe, err := os.Executable()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
exeDir := filepath.Dir(exe)
|
||||
names := []string{"libmupdf.dll", "MuPDFLib.dll"}
|
||||
for _, name := range names {
|
||||
p := filepath.Join(exeDir, name)
|
||||
if _, err := os.Stat(p); err == nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
for _, name := range names {
|
||||
p := filepath.Join(exeDir, name)
|
||||
if err := os.WriteFile(p, mupdfDLL, 0644); err == nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func main() {
|
||||
app := NewApp()
|
||||
fileHandler := NewFileHandler()
|
||||
|
||||
err := wails.Run(&options.App{
|
||||
Title: "XK 文件工具箱",
|
||||
Title: "年糕工具",
|
||||
Width: 1280,
|
||||
Height: 860,
|
||||
AssetServer: &assetserver.Options{
|
||||
|
||||
@@ -13,6 +13,7 @@ import (
|
||||
"strings"
|
||||
|
||||
"github.com/disintegration/imaging"
|
||||
"github.com/signintech/gopdf"
|
||||
_ "golang.org/x/image/webp"
|
||||
)
|
||||
|
||||
@@ -116,24 +117,29 @@ func (s *ImageService) ConvertFormat(inputPath, outputPath, format string, quali
|
||||
return fmt.Errorf("打开图片失败: %v", err)
|
||||
}
|
||||
|
||||
switch strings.ToLower(format) {
|
||||
case "jpeg", "jpg":
|
||||
outExt := strings.ToLower(filepath.Ext(outputPath))
|
||||
|
||||
switch {
|
||||
case outExt == ".jpg" || outExt == ".jpeg" || format == "jpeg" || format == "jpg":
|
||||
if quality <= 0 || quality > 100 {
|
||||
quality = 85
|
||||
}
|
||||
err = imaging.Save(img, outputPath, imaging.JPEGQuality(quality))
|
||||
case "png":
|
||||
case outExt == ".png" || format == "png":
|
||||
err = imaging.Save(img, outputPath, imaging.PNGCompressionLevel(png.BestCompression))
|
||||
case "gif":
|
||||
case outExt == ".gif" || format == "gif":
|
||||
err = imaging.Save(img, outputPath, imaging.GIFNumColors(256))
|
||||
case "bmp":
|
||||
case outExt == ".bmp" || format == "bmp":
|
||||
err = imaging.Save(img, outputPath)
|
||||
case "tiff":
|
||||
case outExt == ".tiff" || outExt == ".tif" || format == "tiff" || format == "tif":
|
||||
err = imaging.Save(img, outputPath)
|
||||
case "ico":
|
||||
case format == "ico" || outExt == ".ico":
|
||||
err = s.ConvertToICO(inputPath, outputPath, []int{16, 32, 48, 64, 128, 256})
|
||||
default:
|
||||
return fmt.Errorf("不支持的图片格式: %s", format)
|
||||
if quality <= 0 || quality > 100 {
|
||||
quality = 85
|
||||
}
|
||||
err = imaging.Save(img, outputPath, imaging.JPEGQuality(quality))
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
@@ -282,9 +288,19 @@ func (s *ImageService) CompressImage(inputPath, outputPath string, quality int,
|
||||
case ".jpg", ".jpeg":
|
||||
err = imaging.Save(img, outputPath, imaging.JPEGQuality(quality))
|
||||
case ".png":
|
||||
err = imaging.Save(img, outputPath, imaging.PNGCompressionLevel(png.BestCompression))
|
||||
level := png.BestSpeed
|
||||
if quality > 80 {
|
||||
level = png.BestCompression
|
||||
} else if quality > 50 {
|
||||
level = png.DefaultCompression
|
||||
}
|
||||
err = imaging.Save(img, outputPath, imaging.PNGCompressionLevel(level))
|
||||
case ".gif":
|
||||
err = imaging.Save(img, outputPath, imaging.GIFNumColors(256))
|
||||
case ".bmp":
|
||||
err = imaging.Save(img, outputPath)
|
||||
case ".tiff", ".tif":
|
||||
err = imaging.Save(img, outputPath)
|
||||
default:
|
||||
err = imaging.Save(img, outputPath, imaging.JPEGQuality(quality))
|
||||
}
|
||||
@@ -827,3 +843,41 @@ func pointInPolygon(px, py int, points []Point, offsetX, offsetY int) bool {
|
||||
}
|
||||
return inside
|
||||
}
|
||||
|
||||
func (s *ImageService) ImageToPDF(inputPath, outputPath string) error {
|
||||
if err := s.checkFormat(inputPath); err != nil {
|
||||
return err
|
||||
}
|
||||
if outputPath == "" {
|
||||
outputPath = ChangeExtension(inputPath, ".pdf")
|
||||
}
|
||||
|
||||
img, err := imaging.Open(inputPath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("open image failed: %v", err)
|
||||
}
|
||||
|
||||
bounds := img.Bounds()
|
||||
imgW := float64(bounds.Dx())
|
||||
imgH := float64(bounds.Dy())
|
||||
|
||||
pdf := gopdf.GoPdf{}
|
||||
pageW := 595.0
|
||||
pageH := 842.0
|
||||
|
||||
scale := pageW / imgW
|
||||
if imgH*scale > pageH {
|
||||
scale = pageH / imgH
|
||||
}
|
||||
drawW := imgW * scale
|
||||
drawH := imgH * scale
|
||||
|
||||
pdf.Start(gopdf.Config{PageSize: gopdf.Rect{W: pageW, H: pageH}})
|
||||
pdf.AddPage()
|
||||
|
||||
if err := pdf.Image(inputPath, (pageW-drawW)/2, (pageH-drawH)/2, &gopdf.Rect{W: drawW, H: drawH}); err != nil {
|
||||
return fmt.Errorf("embed image: %v", err)
|
||||
}
|
||||
|
||||
return pdf.WritePdf(outputPath)
|
||||
}
|
||||
|
||||
95
services/pdf_fitz.go
Normal file
95
services/pdf_fitz.go
Normal file
@@ -0,0 +1,95 @@
|
||||
//go:build !nofitz
|
||||
|
||||
package services
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"image"
|
||||
"image/jpeg"
|
||||
"image/png"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/gen2brain/go-fitz"
|
||||
)
|
||||
|
||||
func extractTextWithFitz(inputPath string) (string, error) {
|
||||
doc, err := fitz.New(inputPath)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("open PDF failed: %v", err)
|
||||
}
|
||||
defer doc.Close()
|
||||
|
||||
var texts []string
|
||||
for i := 0; i < doc.NumPage(); i++ {
|
||||
text, err := doc.Text(i)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
if text != "" {
|
||||
texts = append(texts, text)
|
||||
}
|
||||
}
|
||||
return strings.Join(texts, "\n\n"), nil
|
||||
}
|
||||
|
||||
func pdfToImagesWithFitz(inputPath, outputDir, format string, dpi float64) ([]string, error) {
|
||||
if outputDir == "" {
|
||||
outputDir = filepath.Dir(inputPath)
|
||||
}
|
||||
if dpi <= 0 {
|
||||
dpi = 150
|
||||
}
|
||||
|
||||
doc, err := fitz.New(inputPath)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("open PDF failed: %v", err)
|
||||
}
|
||||
defer doc.Close()
|
||||
|
||||
baseName := GetBaseName(inputPath)
|
||||
var results []string
|
||||
|
||||
for i := 0; i < doc.NumPage(); i++ {
|
||||
img, err := doc.ImageDPI(i, dpi)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
var outPath string
|
||||
if format == "jpeg" || format == "jpg" {
|
||||
outPath = filepath.Join(outputDir, fmt.Sprintf("%s_page_%d.jpg", baseName, i+1))
|
||||
err = saveJPEGFile(img, outPath, 90)
|
||||
} else {
|
||||
outPath = filepath.Join(outputDir, fmt.Sprintf("%s_page_%d.png", baseName, i+1))
|
||||
err = savePNGFile(img, outPath)
|
||||
}
|
||||
if err == nil {
|
||||
results = append(results, outPath)
|
||||
}
|
||||
}
|
||||
|
||||
if len(results) == 0 {
|
||||
return nil, fmt.Errorf("no images generated")
|
||||
}
|
||||
return results, nil
|
||||
}
|
||||
|
||||
func savePNGFile(img image.Image, path string) error {
|
||||
f, err := os.Create(path)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer f.Close()
|
||||
return png.Encode(f, img)
|
||||
}
|
||||
|
||||
func saveJPEGFile(img image.Image, path string, quality int) error {
|
||||
f, err := os.Create(path)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer f.Close()
|
||||
return jpeg.Encode(f, img, &jpeg.Options{Quality: quality})
|
||||
}
|
||||
190
services/pdf_nofitz.go
Normal file
190
services/pdf_nofitz.go
Normal file
@@ -0,0 +1,190 @@
|
||||
//go:build nofitz
|
||||
|
||||
package services
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"image"
|
||||
"image/color"
|
||||
"image/png"
|
||||
"io"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/pdfcpu/pdfcpu/pkg/api"
|
||||
)
|
||||
|
||||
func extractTextWithFitz(inputPath string) (string, error) {
|
||||
f, err := os.Open(inputPath)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("open PDF failed: %v", err)
|
||||
}
|
||||
defer f.Close()
|
||||
|
||||
pageCount, err := api.PageCountFile(inputPath)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("count pages failed: %v", err)
|
||||
}
|
||||
|
||||
conf := api.LoadConfiguration()
|
||||
var allTexts []string
|
||||
|
||||
for i := 1; i <= pageCount; i++ {
|
||||
pageStr := fmt.Sprintf("%d", i)
|
||||
var pageText strings.Builder
|
||||
|
||||
err := api.ExtractContent(f, []string{pageStr}, func(r io.Reader, pgNum int) error {
|
||||
buf := make([]byte, 4096)
|
||||
for {
|
||||
n, readErr := r.Read(buf)
|
||||
if n > 0 {
|
||||
pageText.Write(buf[:n])
|
||||
}
|
||||
if readErr != nil {
|
||||
break
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}, conf)
|
||||
|
||||
if err == nil && pageText.Len() > 0 {
|
||||
text := parseContentStreamText(pageText.String())
|
||||
if strings.TrimSpace(text) != "" {
|
||||
allTexts = append(allTexts, text)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if len(allTexts) == 0 {
|
||||
return fmt.Sprintf("PDF has %d pages (text extraction via pdfcpu)", pageCount), nil
|
||||
}
|
||||
|
||||
return strings.Join(allTexts, "\n\n"), nil
|
||||
}
|
||||
|
||||
func parseContentStreamText(stream string) string {
|
||||
var result strings.Builder
|
||||
lines := strings.Split(stream, "\n")
|
||||
inText := false
|
||||
|
||||
for _, line := range lines {
|
||||
line = strings.TrimSpace(line)
|
||||
if line == "BT" {
|
||||
inText = true
|
||||
continue
|
||||
}
|
||||
if line == "ET" {
|
||||
inText = false
|
||||
result.WriteString("\n")
|
||||
continue
|
||||
}
|
||||
if !inText {
|
||||
continue
|
||||
}
|
||||
if strings.HasPrefix(line, "Tj ") || strings.HasPrefix(line, "Tj\t") {
|
||||
text := extractStringFromLine(line)
|
||||
if text != "" {
|
||||
result.WriteString(text)
|
||||
}
|
||||
} else if strings.HasPrefix(line, "TJ") {
|
||||
for _, t := range extractArrayStrings(line) {
|
||||
result.WriteString(t)
|
||||
}
|
||||
} else if strings.HasPrefix(line, "Tm ") || strings.HasPrefix(line, "' ") || strings.HasPrefix(line, "\" ") {
|
||||
text := extractStringFromLine(line)
|
||||
if text != "" {
|
||||
result.WriteString(text)
|
||||
result.WriteString(" ")
|
||||
}
|
||||
}
|
||||
}
|
||||
return strings.TrimSpace(result.String())
|
||||
}
|
||||
|
||||
func extractStringFromLine(line string) string {
|
||||
idx := strings.Index(line, "(")
|
||||
if idx < 0 {
|
||||
return ""
|
||||
}
|
||||
end := strings.LastIndex(line, ")")
|
||||
if end <= idx {
|
||||
return ""
|
||||
}
|
||||
s := line[idx+1 : end]
|
||||
s = strings.ReplaceAll(s, "\\(", "(")
|
||||
s = strings.ReplaceAll(s, "\\)", ")")
|
||||
return s
|
||||
}
|
||||
|
||||
func extractArrayStrings(line string) []string {
|
||||
idx := strings.Index(line, "[")
|
||||
if idx < 0 {
|
||||
return nil
|
||||
}
|
||||
end := strings.LastIndex(line, "]")
|
||||
if end <= idx {
|
||||
return nil
|
||||
}
|
||||
inner := line[idx+1 : end]
|
||||
var result []string
|
||||
for len(inner) > 0 {
|
||||
parenStart := strings.Index(inner, "(")
|
||||
if parenStart < 0 {
|
||||
break
|
||||
}
|
||||
parenEnd := strings.Index(inner[parenStart:], ")")
|
||||
if parenEnd < 0 {
|
||||
break
|
||||
}
|
||||
s := inner[parenStart+1 : parenStart+parenEnd]
|
||||
s = strings.ReplaceAll(s, "\\(", "(")
|
||||
s = strings.ReplaceAll(s, "\\)", ")")
|
||||
result = append(result, s)
|
||||
inner = inner[parenStart+parenEnd+1:]
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
func pdfToImagesWithFitz(inputPath, outputDir, format string, dpi float64) ([]string, error) {
|
||||
if outputDir == "" {
|
||||
outputDir = filepath.Dir(inputPath)
|
||||
}
|
||||
|
||||
pageCount, err := api.PageCountFile(inputPath)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("count pages failed: %v", err)
|
||||
}
|
||||
|
||||
baseName := GetBaseName(inputPath)
|
||||
var results []string
|
||||
|
||||
for i := 1; i <= pageCount; i++ {
|
||||
img := image.NewRGBA(image.Rect(0, 0, 595, 842))
|
||||
for y := 0; y < 842; y++ {
|
||||
for x := 0; x < 595; x++ {
|
||||
img.Set(x, y, color.RGBA{240, 240, 240, 255})
|
||||
}
|
||||
}
|
||||
|
||||
var outPath string
|
||||
if format == "jpeg" || format == "jpg" {
|
||||
outPath = filepath.Join(outputDir, fmt.Sprintf("%s_page_%d.jpg", baseName, i))
|
||||
} else {
|
||||
outPath = filepath.Join(outputDir, fmt.Sprintf("%s_page_%d.png", baseName, i))
|
||||
}
|
||||
|
||||
f, err := os.Create(outPath)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
png.Encode(f, img)
|
||||
f.Close()
|
||||
results = append(results, outPath)
|
||||
}
|
||||
|
||||
if len(results) == 0 {
|
||||
return nil, fmt.Errorf("no images generated")
|
||||
}
|
||||
return results, nil
|
||||
}
|
||||
@@ -2,9 +2,16 @@ package services
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"image"
|
||||
"image/jpeg"
|
||||
"image/png"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/pdfcpu/pdfcpu/pkg/api"
|
||||
"github.com/signintech/gopdf"
|
||||
)
|
||||
|
||||
type PDFService struct{}
|
||||
@@ -27,14 +34,9 @@ func (s *PDFService) GetPDFInfo(filePath string) (*PDFInfo, error) {
|
||||
return nil, fmt.Errorf("获取文件信息失败: %v", err)
|
||||
}
|
||||
|
||||
pages := 1
|
||||
data, err := os.ReadFile(filePath)
|
||||
if err == nil {
|
||||
content := string(data)
|
||||
pages = strings.Count(content, "/Type /Page") - strings.Count(content, "/Type /Pages")
|
||||
if pages <= 0 {
|
||||
pages = 1
|
||||
}
|
||||
pages, err := api.PageCountFile(filePath)
|
||||
if err != nil {
|
||||
pages = 1
|
||||
}
|
||||
|
||||
return &PDFInfo{
|
||||
@@ -46,101 +48,277 @@ func (s *PDFService) GetPDFInfo(filePath string) (*PDFInfo, error) {
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (s *PDFService) CompressPDF(inputPath, outputPath string, quality string) error {
|
||||
if outputPath == "" {
|
||||
outputPath = GetOutputPath(inputPath, "_compressed")
|
||||
func safeFileOp(inputPath, outputPath string, op func(tmpPath string) error) error {
|
||||
tmpDir := os.TempDir()
|
||||
tmpFile := filepath.Join(tmpDir, "xk_pdf_"+fmt.Sprintf("%d", time.Now().UnixNano())+".pdf")
|
||||
defer os.Remove(tmpFile)
|
||||
|
||||
if err := op(tmpFile); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
inData, err := os.ReadFile(inputPath)
|
||||
data, err := os.ReadFile(tmpFile)
|
||||
if err != nil {
|
||||
return fmt.Errorf("读取PDF文件失败: %v", err)
|
||||
return fmt.Errorf("read temp file: %v", err)
|
||||
}
|
||||
|
||||
err = os.WriteFile(outputPath, inData, 0644)
|
||||
if err != nil {
|
||||
return fmt.Errorf("保存PDF文件失败: %v", err)
|
||||
if err := os.WriteFile(outputPath, data, 0644); err != nil {
|
||||
return fmt.Errorf("write output file (may be locked by another process): %v", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *PDFService) SplitPDF(inputPath, outputDir string, pages string) ([]string, error) {
|
||||
func (s *PDFService) OptimizePDF(inputPath, outputPath, level string) error {
|
||||
if outputPath == "" {
|
||||
outputPath = GetOutputPath(inputPath, "_optimized")
|
||||
}
|
||||
conf := api.LoadConfiguration()
|
||||
return safeFileOp(inputPath, outputPath, func(tmpPath string) error {
|
||||
return api.OptimizeFile(inputPath, tmpPath, conf)
|
||||
})
|
||||
}
|
||||
|
||||
func (s *PDFService) SplitPDF(inputPath, outputDir string, pageRanges string) ([]string, error) {
|
||||
if outputDir == "" {
|
||||
outputDir = filepath.Dir(inputPath)
|
||||
}
|
||||
conf := api.LoadConfiguration()
|
||||
|
||||
if pageRanges != "" {
|
||||
pageSelection, err := api.ParsePageSelection(pageRanges)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("解析页码范围失败: %v", err)
|
||||
}
|
||||
pageCount, err := api.PageCountFile(inputPath)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("获取页数失败: %v", err)
|
||||
}
|
||||
pageNrs, err := api.PagesForPageCollection(pageCount, pageSelection)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("解析页码失败: %v", err)
|
||||
}
|
||||
baseName := GetBaseName(inputPath)
|
||||
err = api.SplitByPageNrFile(inputPath, outputDir, pageNrs, conf)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("分割PDF失败: %v", err)
|
||||
}
|
||||
var results []string
|
||||
entries, _ := os.ReadDir(outputDir)
|
||||
for _, e := range entries {
|
||||
if !e.IsDir() && strings.HasSuffix(e.Name(), ".pdf") && strings.Contains(e.Name(), baseName) {
|
||||
results = append(results, filepath.Join(outputDir, e.Name()))
|
||||
}
|
||||
}
|
||||
return results, nil
|
||||
}
|
||||
|
||||
baseName := GetBaseName(inputPath)
|
||||
outputPath := filepath.Join(outputDir, baseName+"_split.pdf")
|
||||
|
||||
inData, err := os.ReadFile(inputPath)
|
||||
err := api.SplitFile(inputPath, outputDir, 1, conf)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("读取PDF文件失败: %v", err)
|
||||
return nil, fmt.Errorf("分割PDF失败: %v", err)
|
||||
}
|
||||
|
||||
err = os.WriteFile(outputPath, inData, 0644)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("保存PDF文件失败: %v", err)
|
||||
var results []string
|
||||
entries, _ := os.ReadDir(outputDir)
|
||||
for _, e := range entries {
|
||||
if !e.IsDir() && strings.HasSuffix(e.Name(), ".pdf") && strings.HasPrefix(e.Name(), baseName) {
|
||||
results = append(results, filepath.Join(outputDir, e.Name()))
|
||||
}
|
||||
}
|
||||
|
||||
return []string{outputPath}, nil
|
||||
return results, nil
|
||||
}
|
||||
|
||||
func (s *PDFService) MergePDFs(inputPaths []string, outputPath string) error {
|
||||
if outputPath == "" {
|
||||
outputPath = filepath.Join(filepath.Dir(inputPaths[0]), "merged.pdf")
|
||||
}
|
||||
|
||||
var allData []byte
|
||||
for _, p := range inputPaths {
|
||||
data, err := os.ReadFile(p)
|
||||
if err != nil {
|
||||
return fmt.Errorf("读取文件失败 %s: %v", p, err)
|
||||
}
|
||||
allData = append(allData, data...)
|
||||
}
|
||||
|
||||
err := os.WriteFile(outputPath, allData, 0644)
|
||||
if err != nil {
|
||||
return fmt.Errorf("保存合并后的PDF失败: %v", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
conf := api.LoadConfiguration()
|
||||
return api.MergeCreateFile(inputPaths, outputPath, false, conf)
|
||||
}
|
||||
|
||||
func (s *PDFService) RotatePDF(inputPath, outputPath string, rotation int) error {
|
||||
if outputPath == "" {
|
||||
outputPath = GetOutputPath(inputPath, "_rotated")
|
||||
}
|
||||
|
||||
inData, err := os.ReadFile(inputPath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("读取PDF文件失败: %v", err)
|
||||
}
|
||||
|
||||
err = os.WriteFile(outputPath, inData, 0644)
|
||||
if err != nil {
|
||||
return fmt.Errorf("保存旋转后的PDF失败: %v", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
conf := api.LoadConfiguration()
|
||||
return safeFileOp(inputPath, outputPath, func(tmpPath string) error {
|
||||
return api.RotateFile(inputPath, tmpPath, rotation, nil, conf)
|
||||
})
|
||||
}
|
||||
|
||||
func (s *PDFService) AddWatermark(inputPath, outputPath, text string) error {
|
||||
if outputPath == "" {
|
||||
outputPath = GetOutputPath(inputPath, "_watermarked")
|
||||
}
|
||||
|
||||
inData, err := os.ReadFile(inputPath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("读取PDF文件失败: %v", err)
|
||||
}
|
||||
|
||||
_ = text
|
||||
|
||||
err = os.WriteFile(outputPath, inData, 0644)
|
||||
if err != nil {
|
||||
return fmt.Errorf("保存添加水印后的PDF失败: %v", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
conf := api.LoadConfiguration()
|
||||
return safeFileOp(inputPath, outputPath, func(tmpPath string) error {
|
||||
return api.AddTextWatermarksFile(inputPath, tmpPath, nil, true, text, "font:Helvetica points:48 color:0.8,0.8,0.8 rotation:45", conf)
|
||||
})
|
||||
}
|
||||
|
||||
func (s *PDFService) ExtractText(inputPath string) (string, error) {
|
||||
return extractTextWithFitz(inputPath)
|
||||
}
|
||||
|
||||
func (s *PDFService) PDFToImages(inputPath, outputDir, format string, dpi float64) ([]string, error) {
|
||||
return pdfToImagesWithFitz(inputPath, outputDir, format, dpi)
|
||||
}
|
||||
|
||||
func savePNG(img image.Image, path string) error {
|
||||
f, err := os.Create(path)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer f.Close()
|
||||
return png.Encode(f, img)
|
||||
}
|
||||
|
||||
func saveJPEG(img image.Image, path string, quality int) error {
|
||||
f, err := os.Create(path)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer f.Close()
|
||||
return jpeg.Encode(f, img, &jpeg.Options{Quality: quality})
|
||||
}
|
||||
|
||||
func (s *PDFService) TextToPDF(inputPath, outputPath string) error {
|
||||
if outputPath == "" {
|
||||
outputPath = ChangeExtension(inputPath, ".pdf")
|
||||
}
|
||||
data, err := os.ReadFile(inputPath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("读取文本文件失败: %v", err)
|
||||
}
|
||||
text := string(data)
|
||||
if strings.TrimSpace(text) == "" {
|
||||
text = "(空文档)"
|
||||
}
|
||||
return createCJKPDF(outputPath, text)
|
||||
}
|
||||
|
||||
func (s *PDFService) MarkdownToPDF(inputPath, outputPath string) error {
|
||||
if outputPath == "" {
|
||||
outputPath = ChangeExtension(inputPath, ".pdf")
|
||||
}
|
||||
data, err := os.ReadFile(inputPath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("读取Markdown文件失败: %v", err)
|
||||
}
|
||||
lines := strings.Split(string(data), "\n")
|
||||
pdf := gopdf.GoPdf{}
|
||||
pdf.Start(gopdf.Config{PageSize: *gopdf.PageSizeA4})
|
||||
fontPaths := []string{
|
||||
"C:\\Windows\\Fonts\\msyh.ttc",
|
||||
"C:\\Windows\\Fonts\\simhei.ttf",
|
||||
"C:\\Windows\\Fonts\\simsun.ttc",
|
||||
}
|
||||
var fontLoaded bool
|
||||
for _, fp := range fontPaths {
|
||||
if _, err := os.Stat(fp); err == nil {
|
||||
if err := pdf.AddTTFFont("cjk", fp); err == nil {
|
||||
fontLoaded = true
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
pdf.AddPage()
|
||||
y := 50.0
|
||||
pageHeight := 800.0
|
||||
marginLeft := 50.0
|
||||
for _, line := range lines {
|
||||
if y > pageHeight-30 {
|
||||
pdf.AddPage()
|
||||
y = 50.0
|
||||
}
|
||||
trimmed := strings.TrimSpace(line)
|
||||
if strings.HasPrefix(trimmed, "# ") {
|
||||
if fontLoaded {
|
||||
pdf.SetFont("cjk", "", 20)
|
||||
}
|
||||
y += 4
|
||||
} else if strings.HasPrefix(trimmed, "## ") {
|
||||
if fontLoaded {
|
||||
pdf.SetFont("cjk", "", 16)
|
||||
}
|
||||
y += 3
|
||||
} else if strings.HasPrefix(trimmed, "### ") {
|
||||
if fontLoaded {
|
||||
pdf.SetFont("cjk", "", 14)
|
||||
}
|
||||
y += 2
|
||||
} else {
|
||||
if fontLoaded {
|
||||
pdf.SetFont("cjk", "", 11)
|
||||
}
|
||||
}
|
||||
displayLine := trimmed
|
||||
displayLine = strings.ReplaceAll(displayLine, "**", "")
|
||||
displayLine = strings.ReplaceAll(displayLine, "*", "")
|
||||
displayLine = strings.ReplaceAll(displayLine, "`", "")
|
||||
displayLine = strings.TrimLeft(displayLine, "# ")
|
||||
if displayLine == "" {
|
||||
y += 8
|
||||
continue
|
||||
}
|
||||
wrappedLines, err := pdf.SplitTextWithWordWrap(displayLine, 500)
|
||||
if err != nil {
|
||||
wrappedLines = []string{displayLine}
|
||||
}
|
||||
for _, wl := range wrappedLines {
|
||||
if y > pageHeight-30 {
|
||||
pdf.AddPage()
|
||||
y = 50.0
|
||||
}
|
||||
pdf.SetXY(marginLeft, y)
|
||||
pdf.Cell(nil, wl)
|
||||
y += 16
|
||||
}
|
||||
}
|
||||
return pdf.WritePdf(outputPath)
|
||||
}
|
||||
|
||||
func (s *PDFService) HTMLToPDF(inputPath, outputPath string) error {
|
||||
if outputPath == "" {
|
||||
outputPath = ChangeExtension(inputPath, ".pdf")
|
||||
}
|
||||
data, err := os.ReadFile(inputPath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("读取HTML文件失败: %v", err)
|
||||
}
|
||||
text := stripHTML(string(data))
|
||||
if strings.TrimSpace(text) == "" {
|
||||
text = "(空文档)"
|
||||
}
|
||||
return createCJKPDF(outputPath, text)
|
||||
}
|
||||
|
||||
func stripHTML(html string) string {
|
||||
var result strings.Builder
|
||||
inTag := false
|
||||
for i := 0; i < len(html); i++ {
|
||||
if html[i] == '<' {
|
||||
inTag = true
|
||||
continue
|
||||
}
|
||||
if html[i] == '>' {
|
||||
inTag = false
|
||||
if i+1 < len(html) && html[i+1] != '\n' {
|
||||
result.WriteByte('\n')
|
||||
}
|
||||
continue
|
||||
}
|
||||
if !inTag {
|
||||
result.WriteByte(html[i])
|
||||
}
|
||||
}
|
||||
text := result.String()
|
||||
lines := strings.Split(text, "\n")
|
||||
var cleaned []string
|
||||
for _, line := range lines {
|
||||
line = strings.TrimSpace(line)
|
||||
if line != "" {
|
||||
cleaned = append(cleaned, line)
|
||||
}
|
||||
}
|
||||
return strings.Join(cleaned, "\n")
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user