初始化
This commit is contained in:
261
services/excel_service.go
Normal file
261
services/excel_service.go
Normal file
@@ -0,0 +1,261 @@
|
||||
package services
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"github.com/xuri/excelize/v2"
|
||||
)
|
||||
|
||||
type ExcelService struct{}
|
||||
|
||||
func NewExcelService() *ExcelService {
|
||||
return &ExcelService{}
|
||||
}
|
||||
|
||||
type ExcelInfo struct {
|
||||
Sheets []string `json:"sheets"`
|
||||
SheetCount int `json:"sheetCount"`
|
||||
FilePath string `json:"filePath"`
|
||||
Size int64 `json:"size"`
|
||||
}
|
||||
|
||||
type SheetData struct {
|
||||
Name string `json:"name"`
|
||||
Rows int `json:"rows"`
|
||||
Columns int `json:"columns"`
|
||||
Data [][]string `json:"data"`
|
||||
}
|
||||
|
||||
func (s *ExcelService) GetExcelInfo(filePath string) (*ExcelInfo, error) {
|
||||
f, err := excelize.OpenFile(filePath)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("打开Excel文件失败: %v", err)
|
||||
}
|
||||
defer f.Close()
|
||||
|
||||
fileInfo, err := os.Stat(filePath)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("获取文件信息失败: %v", err)
|
||||
}
|
||||
|
||||
sheets := f.GetSheetList()
|
||||
|
||||
return &ExcelInfo{
|
||||
Sheets: sheets,
|
||||
SheetCount: len(sheets),
|
||||
FilePath: filePath,
|
||||
Size: fileInfo.Size(),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (s *ExcelService) GetSheetData(filePath, sheetName string) (*SheetData, error) {
|
||||
f, err := excelize.OpenFile(filePath)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("打开Excel文件失败: %v", err)
|
||||
}
|
||||
defer f.Close()
|
||||
|
||||
if sheetName == "" {
|
||||
sheets := f.GetSheetList()
|
||||
if len(sheets) == 0 {
|
||||
return nil, fmt.Errorf("工作簿中没有工作表")
|
||||
}
|
||||
sheetName = sheets[0]
|
||||
}
|
||||
|
||||
rows, err := f.GetRows(sheetName)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("读取工作表失败: %v", err)
|
||||
}
|
||||
|
||||
maxCols := 0
|
||||
for _, row := range rows {
|
||||
if len(row) > maxCols {
|
||||
maxCols = len(row)
|
||||
}
|
||||
}
|
||||
|
||||
return &SheetData{
|
||||
Name: sheetName,
|
||||
Rows: len(rows),
|
||||
Columns: maxCols,
|
||||
Data: rows,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (s *ExcelService) ConvertToCSV(inputPath, outputPath string) error {
|
||||
if outputPath == "" {
|
||||
outputPath = ChangeExtension(inputPath, ".csv")
|
||||
}
|
||||
|
||||
f, err := excelize.OpenFile(inputPath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("打开Excel文件失败: %v", err)
|
||||
}
|
||||
defer f.Close()
|
||||
|
||||
sheets := f.GetSheetList()
|
||||
if len(sheets) == 0 {
|
||||
return fmt.Errorf("工作簿中没有工作表")
|
||||
}
|
||||
|
||||
rows, err := f.GetRows(sheets[0])
|
||||
if err != nil {
|
||||
return fmt.Errorf("读取工作表失败: %v", err)
|
||||
}
|
||||
|
||||
outFile, err := os.Create(outputPath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("创建输出文件失败: %v", err)
|
||||
}
|
||||
defer outFile.Close()
|
||||
|
||||
for _, row := range rows {
|
||||
line := ""
|
||||
for i, cell := range row {
|
||||
if i > 0 {
|
||||
line += ","
|
||||
}
|
||||
if containsComma(cell) {
|
||||
line += "\"" + cell + "\""
|
||||
} else {
|
||||
line += cell
|
||||
}
|
||||
}
|
||||
_, err := outFile.WriteString(line + "\n")
|
||||
if err != nil {
|
||||
return fmt.Errorf("写入CSV失败: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *ExcelService) ConvertToJSON(inputPath, outputPath string) error {
|
||||
if outputPath == "" {
|
||||
outputPath = ChangeExtension(inputPath, ".json")
|
||||
}
|
||||
|
||||
f, err := excelize.OpenFile(inputPath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("打开Excel文件失败: %v", err)
|
||||
}
|
||||
defer f.Close()
|
||||
|
||||
sheets := f.GetSheetList()
|
||||
if len(sheets) == 0 {
|
||||
return fmt.Errorf("工作簿中没有工作表")
|
||||
}
|
||||
|
||||
rows, err := f.GetRows(sheets[0])
|
||||
if err != nil {
|
||||
return fmt.Errorf("读取工作表失败: %v", err)
|
||||
}
|
||||
|
||||
if len(rows) < 2 {
|
||||
return fmt.Errorf("工作表数据不足")
|
||||
}
|
||||
|
||||
headers := rows[0]
|
||||
var jsonData []map[string]string
|
||||
|
||||
for _, row := range rows[1:] {
|
||||
record := make(map[string]string)
|
||||
for i, header := range headers {
|
||||
if i < len(row) {
|
||||
record[header] = row[i]
|
||||
} else {
|
||||
record[header] = ""
|
||||
}
|
||||
}
|
||||
jsonData = append(jsonData, record)
|
||||
}
|
||||
|
||||
outFile, err := os.Create(outputPath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("创建输出文件失败: %v", err)
|
||||
}
|
||||
defer outFile.Close()
|
||||
|
||||
_, err = outFile.WriteString("[\n")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for i, record := range jsonData {
|
||||
_, err = outFile.WriteString(" {\n")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
j := 0
|
||||
for key, value := range record {
|
||||
_, err = outFile.WriteString(fmt.Sprintf(" \"%s\": \"%s\"", key, value))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if j < len(record)-1 {
|
||||
_, err = outFile.WriteString(",\n")
|
||||
} else {
|
||||
_, err = outFile.WriteString("\n")
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
j++
|
||||
}
|
||||
|
||||
_, err = outFile.WriteString(" }")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if i < len(jsonData)-1 {
|
||||
_, err = outFile.WriteString(",\n")
|
||||
} else {
|
||||
_, err = outFile.WriteString("\n")
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
_, err = outFile.WriteString("]")
|
||||
return err
|
||||
}
|
||||
|
||||
func (s *ExcelService) CreateExcelFromData(outputPath string, data [][]string) error {
|
||||
f := excelize.NewFile()
|
||||
defer f.Close()
|
||||
|
||||
sheetName := "Sheet1"
|
||||
index, err := f.NewSheet(sheetName)
|
||||
if err != nil {
|
||||
return fmt.Errorf("创建工作表失败: %v", err)
|
||||
}
|
||||
|
||||
f.SetActiveSheet(index)
|
||||
|
||||
for i, row := range data {
|
||||
for j, cell := range row {
|
||||
cellName, _ := excelize.CoordinatesToCellName(j+1, i+1)
|
||||
f.SetCellValue(sheetName, cellName, cell)
|
||||
}
|
||||
}
|
||||
|
||||
if err := f.SaveAs(outputPath); err != nil {
|
||||
return fmt.Errorf("保存Excel文件失败: %v", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func containsComma(s string) bool {
|
||||
for _, c := range s {
|
||||
if c == ',' {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
829
services/image_service.go
Normal file
829
services/image_service.go
Normal file
@@ -0,0 +1,829 @@
|
||||
package services
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/base64"
|
||||
"fmt"
|
||||
"image"
|
||||
"image/color"
|
||||
"image/png"
|
||||
"math"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/disintegration/imaging"
|
||||
_ "golang.org/x/image/webp"
|
||||
)
|
||||
|
||||
// supportedFormats lists formats that imaging.Open() can handle
|
||||
var supportedFormats = map[string]bool{
|
||||
".jpg": true,
|
||||
".jpeg": true,
|
||||
".png": true,
|
||||
".gif": true,
|
||||
".bmp": true,
|
||||
".tiff": true,
|
||||
".tif": true,
|
||||
".webp": true,
|
||||
}
|
||||
|
||||
func (s *ImageService) checkFormat(filePath string) error {
|
||||
ext := strings.ToLower(filepath.Ext(filePath))
|
||||
if ext == ".ico" {
|
||||
return fmt.Errorf("ICO 格式暂不支持,请先转换为 PNG 或 JPG 格式")
|
||||
}
|
||||
if !supportedFormats[ext] {
|
||||
return fmt.Errorf("不支持的图片格式: %s", ext)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
type ImageService struct{}
|
||||
|
||||
func NewImageService() *ImageService {
|
||||
return &ImageService{}
|
||||
}
|
||||
|
||||
type ImageInfo struct {
|
||||
Width int `json:"width"`
|
||||
Height int `json:"height"`
|
||||
Format string `json:"format"`
|
||||
FilePath string `json:"filePath"`
|
||||
Size int64 `json:"size"`
|
||||
}
|
||||
|
||||
func (s *ImageService) GetImageInfo(filePath string) (*ImageInfo, error) {
|
||||
file, err := os.Open(filePath)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("打开图片文件失败: %v", err)
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
config, format, err := image.DecodeConfig(file)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("解码图片配置失败: %v", err)
|
||||
}
|
||||
|
||||
fileInfo, err := os.Stat(filePath)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("获取文件信息失败: %v", err)
|
||||
}
|
||||
|
||||
return &ImageInfo{
|
||||
Width: config.Width,
|
||||
Height: config.Height,
|
||||
Format: format,
|
||||
FilePath: filePath,
|
||||
Size: fileInfo.Size(),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (s *ImageService) GetImageBase64(filePath string) (string, error) {
|
||||
data, err := os.ReadFile(filePath)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
ext := strings.ToLower(filepath.Ext(filePath))
|
||||
mimeType := "image/png"
|
||||
switch ext {
|
||||
case ".jpg", ".jpeg":
|
||||
mimeType = "image/jpeg"
|
||||
case ".gif":
|
||||
mimeType = "image/gif"
|
||||
case ".bmp":
|
||||
mimeType = "image/bmp"
|
||||
case ".webp":
|
||||
mimeType = "image/webp"
|
||||
case ".ico":
|
||||
mimeType = "image/x-icon"
|
||||
}
|
||||
|
||||
return "data:" + mimeType + ";base64," + base64.StdEncoding.EncodeToString(data), nil
|
||||
}
|
||||
|
||||
func (s *ImageService) ConvertFormat(inputPath, outputPath, format string, quality int) error {
|
||||
if err := s.checkFormat(inputPath); err != nil {
|
||||
return err
|
||||
}
|
||||
if outputPath == "" {
|
||||
outputPath = ChangeExtension(inputPath, "."+format)
|
||||
}
|
||||
|
||||
img, err := imaging.Open(inputPath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("打开图片失败: %v", err)
|
||||
}
|
||||
|
||||
switch strings.ToLower(format) {
|
||||
case "jpeg", "jpg":
|
||||
if quality <= 0 || quality > 100 {
|
||||
quality = 85
|
||||
}
|
||||
err = imaging.Save(img, outputPath, imaging.JPEGQuality(quality))
|
||||
case "png":
|
||||
err = imaging.Save(img, outputPath, imaging.PNGCompressionLevel(png.BestCompression))
|
||||
case "gif":
|
||||
err = imaging.Save(img, outputPath, imaging.GIFNumColors(256))
|
||||
case "bmp":
|
||||
err = imaging.Save(img, outputPath)
|
||||
case "tiff":
|
||||
err = imaging.Save(img, outputPath)
|
||||
case "ico":
|
||||
err = s.ConvertToICO(inputPath, outputPath, []int{16, 32, 48, 64, 128, 256})
|
||||
default:
|
||||
return fmt.Errorf("不支持的图片格式: %s", format)
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return fmt.Errorf("保存图片失败: %v", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *ImageService) ConvertToICO(inputPath, outputPath string, sizes []int) error {
|
||||
if outputPath == "" {
|
||||
outputPath = ChangeExtension(inputPath, ".ico")
|
||||
}
|
||||
|
||||
srcImg, err := imaging.Open(inputPath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("打开图片失败: %v", err)
|
||||
}
|
||||
|
||||
var images []image.Image
|
||||
for _, size := range sizes {
|
||||
resized := imaging.Resize(srcImg, size, size, imaging.Lanczos)
|
||||
images = append(images, resized)
|
||||
}
|
||||
|
||||
return encodeICO(outputPath, images)
|
||||
}
|
||||
|
||||
func encodeICO(outputPath string, images []image.Image) error {
|
||||
f, err := os.Create(outputPath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer f.Close()
|
||||
|
||||
var pngDataBuffers [][]byte
|
||||
for _, img := range images {
|
||||
buf := new(bytes.Buffer)
|
||||
if err := png.Encode(buf, img); err != nil {
|
||||
return err
|
||||
}
|
||||
pngDataBuffers = append(pngDataBuffers, buf.Bytes())
|
||||
}
|
||||
|
||||
header := make([]byte, 6)
|
||||
header[0] = 0
|
||||
header[1] = 0
|
||||
header[2] = 1
|
||||
header[3] = 0
|
||||
count := uint16(len(images))
|
||||
header[4] = byte(count)
|
||||
header[5] = byte(count >> 8)
|
||||
|
||||
if _, err := f.Write(header); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
offset := uint32(6 + len(images)*16)
|
||||
for i, img := range images {
|
||||
bounds := img.Bounds()
|
||||
w := bounds.Dx()
|
||||
h := bounds.Dy()
|
||||
entry := make([]byte, 16)
|
||||
if w > 255 {
|
||||
w = 0
|
||||
}
|
||||
if h > 255 {
|
||||
h = 0
|
||||
}
|
||||
entry[0] = byte(w)
|
||||
entry[1] = byte(h)
|
||||
entry[2] = 0
|
||||
entry[3] = 0
|
||||
entry[4] = 1
|
||||
entry[5] = 0
|
||||
entry[6] = 32
|
||||
entry[7] = 0
|
||||
dataSize := uint32(len(pngDataBuffers[i]))
|
||||
entry[8] = byte(dataSize)
|
||||
entry[9] = byte(dataSize >> 8)
|
||||
entry[10] = byte(dataSize >> 16)
|
||||
entry[11] = byte(dataSize >> 24)
|
||||
entry[12] = byte(offset)
|
||||
entry[13] = byte(offset >> 8)
|
||||
entry[14] = byte(offset >> 16)
|
||||
entry[15] = byte(offset >> 24)
|
||||
if _, err := f.Write(entry); err != nil {
|
||||
return err
|
||||
}
|
||||
offset += uint32(len(pngDataBuffers[i]))
|
||||
_ = i
|
||||
}
|
||||
|
||||
for _, data := range pngDataBuffers {
|
||||
if _, err := f.Write(data); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *ImageService) CompressImage(inputPath, outputPath string, quality int, maxWidth, maxHeight int) error {
|
||||
if err := s.checkFormat(inputPath); err != nil {
|
||||
return err
|
||||
}
|
||||
if quality < 1 {
|
||||
quality = 1
|
||||
}
|
||||
if quality > 100 {
|
||||
quality = 100
|
||||
}
|
||||
if outputPath == "" {
|
||||
outputPath = GetOutputPath(inputPath, "_compressed")
|
||||
}
|
||||
|
||||
img, err := imaging.Open(inputPath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("打开图片失败: %v", err)
|
||||
}
|
||||
|
||||
if maxWidth > 0 || maxHeight > 0 {
|
||||
bounds := img.Bounds()
|
||||
width := bounds.Dx()
|
||||
height := bounds.Dy()
|
||||
|
||||
newWidth := width
|
||||
newHeight := height
|
||||
|
||||
if maxWidth > 0 && width > maxWidth {
|
||||
newWidth = maxWidth
|
||||
newHeight = height * maxWidth / width
|
||||
}
|
||||
|
||||
if maxHeight > 0 && newHeight > maxHeight {
|
||||
newWidth = newWidth * maxHeight / newHeight
|
||||
newHeight = maxHeight
|
||||
}
|
||||
|
||||
img = imaging.Resize(img, newWidth, newHeight, imaging.Lanczos)
|
||||
}
|
||||
|
||||
ext := strings.ToLower(filepath.Ext(outputPath))
|
||||
|
||||
switch ext {
|
||||
case ".jpg", ".jpeg":
|
||||
err = imaging.Save(img, outputPath, imaging.JPEGQuality(quality))
|
||||
case ".png":
|
||||
err = imaging.Save(img, outputPath, imaging.PNGCompressionLevel(png.BestCompression))
|
||||
case ".gif":
|
||||
err = imaging.Save(img, outputPath, imaging.GIFNumColors(256))
|
||||
default:
|
||||
err = imaging.Save(img, outputPath, imaging.JPEGQuality(quality))
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return fmt.Errorf("保存压缩图片失败: %v", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *ImageService) ResizeImage(inputPath, outputPath string, width, height int, maintainRatio bool) error {
|
||||
if err := s.checkFormat(inputPath); err != nil {
|
||||
return err
|
||||
}
|
||||
if width <= 0 || height <= 0 {
|
||||
return fmt.Errorf("宽度和高度必须大于 0")
|
||||
}
|
||||
if outputPath == "" {
|
||||
outputPath = GetOutputPath(inputPath, "_resized")
|
||||
}
|
||||
|
||||
img, err := imaging.Open(inputPath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("打开图片失败: %v", err)
|
||||
}
|
||||
|
||||
if maintainRatio {
|
||||
img = imaging.Fit(img, width, height, imaging.Lanczos)
|
||||
} else {
|
||||
img = imaging.Resize(img, width, height, imaging.Lanczos)
|
||||
}
|
||||
|
||||
err = imaging.Save(img, outputPath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("保存调整大小后的图片失败: %v", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *ImageService) RotateImage(inputPath, outputPath string, angle float64, bgColor string) error {
|
||||
if err := s.checkFormat(inputPath); err != nil {
|
||||
return err
|
||||
}
|
||||
if outputPath == "" {
|
||||
outputPath = GetOutputPath(inputPath, "_rotated")
|
||||
}
|
||||
|
||||
img, err := imaging.Open(inputPath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("打开图片失败: %v", err)
|
||||
}
|
||||
|
||||
var fillColor color.Color
|
||||
switch bgColor {
|
||||
case "white":
|
||||
fillColor = color.White
|
||||
case "black":
|
||||
fillColor = color.Black
|
||||
default:
|
||||
fillColor = color.Transparent
|
||||
}
|
||||
|
||||
img = imaging.Rotate(img, angle, fillColor)
|
||||
|
||||
err = imaging.Save(img, outputPath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("保存旋转后的图片失败: %v", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *ImageService) AddGrayscale(inputPath, outputPath string) error {
|
||||
return s.AdjustGrayscale(inputPath, outputPath, 100)
|
||||
}
|
||||
|
||||
func (s *ImageService) AdjustGrayscale(inputPath, outputPath string, intensity int) error {
|
||||
if err := s.checkFormat(inputPath); err != nil {
|
||||
return err
|
||||
}
|
||||
if outputPath == "" {
|
||||
outputPath = GetOutputPath(inputPath, "_grayscale")
|
||||
}
|
||||
|
||||
img, err := imaging.Open(inputPath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("打开图片失败: %v", err)
|
||||
}
|
||||
|
||||
if intensity >= 100 {
|
||||
img = imaging.Grayscale(img)
|
||||
} else if intensity > 0 {
|
||||
gray := imaging.Grayscale(img)
|
||||
img = blendImages(img, gray, float64(intensity)/100.0)
|
||||
}
|
||||
|
||||
err = imaging.Save(img, outputPath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("保存灰度图片失败: %v", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func blendImages(src, gray image.Image, opacity float64) image.Image {
|
||||
if opacity < 0 {
|
||||
opacity = 0
|
||||
}
|
||||
if opacity > 1 {
|
||||
opacity = 1
|
||||
}
|
||||
bounds := src.Bounds()
|
||||
dst := image.NewRGBA(bounds)
|
||||
for y := bounds.Min.Y; y < bounds.Max.Y; y++ {
|
||||
for x := bounds.Min.X; x < bounds.Max.X; x++ {
|
||||
r1, g1, b1, a1 := src.At(x, y).RGBA()
|
||||
r2, g2, b2, _ := gray.At(x, y).RGBA()
|
||||
r := uint8((float64(r1>>8)*(1-opacity) + float64(r2>>8)*opacity))
|
||||
g := uint8((float64(g1>>8)*(1-opacity) + float64(g2>>8)*opacity))
|
||||
b := uint8((float64(b1>>8)*(1-opacity) + float64(b2>>8)*opacity))
|
||||
a := uint8(a1 >> 8)
|
||||
dst.Set(x, y, color.RGBA{r, g, b, a})
|
||||
}
|
||||
}
|
||||
return dst
|
||||
}
|
||||
|
||||
func (s *ImageService) AdjustBrightness(inputPath, outputPath string, factor float64) error {
|
||||
if err := s.checkFormat(inputPath); err != nil {
|
||||
return err
|
||||
}
|
||||
if outputPath == "" {
|
||||
outputPath = GetOutputPath(inputPath, "_bright")
|
||||
}
|
||||
|
||||
img, err := imaging.Open(inputPath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("打开图片失败: %v", err)
|
||||
}
|
||||
|
||||
img = imaging.AdjustBrightness(img, factor)
|
||||
|
||||
err = imaging.Save(img, outputPath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("保存调整亮度后的图片失败: %v", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *ImageService) AdjustContrast(inputPath, outputPath string, factor float64) error {
|
||||
if err := s.checkFormat(inputPath); err != nil {
|
||||
return err
|
||||
}
|
||||
if outputPath == "" {
|
||||
outputPath = GetOutputPath(inputPath, "_contrast")
|
||||
}
|
||||
|
||||
img, err := imaging.Open(inputPath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("打开图片失败: %v", err)
|
||||
}
|
||||
|
||||
img = imaging.AdjustContrast(img, factor)
|
||||
|
||||
err = imaging.Save(img, outputPath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("保存调整对比度后的图片失败: %v", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *ImageService) AdjustSaturation(inputPath, outputPath string, factor float64) error {
|
||||
if err := s.checkFormat(inputPath); err != nil {
|
||||
return err
|
||||
}
|
||||
if outputPath == "" {
|
||||
outputPath = GetOutputPath(inputPath, "_saturation")
|
||||
}
|
||||
|
||||
img, err := imaging.Open(inputPath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("打开图片失败: %v", err)
|
||||
}
|
||||
|
||||
img = imaging.AdjustSaturation(img, factor)
|
||||
|
||||
err = imaging.Save(img, outputPath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("保存调整饱和度后的图片失败: %v", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *ImageService) FlipH(inputPath, outputPath string) error {
|
||||
if err := s.checkFormat(inputPath); err != nil {
|
||||
return err
|
||||
}
|
||||
if outputPath == "" {
|
||||
outputPath = GetOutputPath(inputPath, "_fliph")
|
||||
}
|
||||
|
||||
img, err := imaging.Open(inputPath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("打开图片失败: %v", err)
|
||||
}
|
||||
|
||||
img = imaging.FlipH(img)
|
||||
|
||||
err = imaging.Save(img, outputPath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("保存水平翻转图片失败: %v", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *ImageService) FlipV(inputPath, outputPath string) error {
|
||||
if err := s.checkFormat(inputPath); err != nil {
|
||||
return err
|
||||
}
|
||||
if outputPath == "" {
|
||||
outputPath = GetOutputPath(inputPath, "_flipv")
|
||||
}
|
||||
|
||||
img, err := imaging.Open(inputPath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("打开图片失败: %v", err)
|
||||
}
|
||||
|
||||
img = imaging.FlipV(img)
|
||||
|
||||
err = imaging.Save(img, outputPath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("保存垂直翻转图片失败: %v", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *ImageService) SharpenImage(inputPath, outputPath string, amount float64) error {
|
||||
if err := s.checkFormat(inputPath); err != nil {
|
||||
return err
|
||||
}
|
||||
if outputPath == "" {
|
||||
outputPath = GetOutputPath(inputPath, "_sharpen")
|
||||
}
|
||||
|
||||
img, err := imaging.Open(inputPath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("打开图片失败: %v", err)
|
||||
}
|
||||
|
||||
img = imaging.Sharpen(img, amount)
|
||||
|
||||
err = imaging.Save(img, outputPath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("保存锐化图片失败: %v", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *ImageService) BlurImage(inputPath, outputPath string, radius float64) error {
|
||||
if err := s.checkFormat(inputPath); err != nil {
|
||||
return err
|
||||
}
|
||||
if outputPath == "" {
|
||||
outputPath = GetOutputPath(inputPath, "_blur")
|
||||
}
|
||||
|
||||
img, err := imaging.Open(inputPath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("打开图片失败: %v", err)
|
||||
}
|
||||
|
||||
img = imaging.Blur(img, radius)
|
||||
|
||||
err = imaging.Save(img, outputPath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("保存模糊图片失败: %v", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *ImageService) InvertImage(inputPath, outputPath string) error {
|
||||
if err := s.checkFormat(inputPath); err != nil {
|
||||
return err
|
||||
}
|
||||
if outputPath == "" {
|
||||
outputPath = GetOutputPath(inputPath, "_invert")
|
||||
}
|
||||
|
||||
img, err := imaging.Open(inputPath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("打开图片失败: %v", err)
|
||||
}
|
||||
|
||||
img = imaging.Invert(img)
|
||||
|
||||
err = imaging.Save(img, outputPath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("保存反色图片失败: %v", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *ImageService) RemoveBackground(inputPath, outputPath string, threshold int, bgColor *ColorRGB) error {
|
||||
if err := s.checkFormat(inputPath); err != nil {
|
||||
return err
|
||||
}
|
||||
if outputPath == "" {
|
||||
outputPath = GetOutputPath(inputPath, "_nobg")
|
||||
}
|
||||
|
||||
if threshold <= 0 {
|
||||
threshold = 30
|
||||
}
|
||||
|
||||
srcImg, err := imaging.Open(inputPath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("打开图片失败: %v", err)
|
||||
}
|
||||
|
||||
bounds := srcImg.Bounds()
|
||||
dst := image.NewRGBA(bounds)
|
||||
|
||||
maxDist := float64(threshold) / 100.0 * 441.67
|
||||
|
||||
for y := bounds.Min.Y; y < bounds.Max.Y; y++ {
|
||||
for x := bounds.Min.X; x < bounds.Max.X; x++ {
|
||||
r, g, b, _ := srcImg.At(x, y).RGBA()
|
||||
r8 := uint8(r >> 8)
|
||||
g8 := uint8(g >> 8)
|
||||
b8 := uint8(b >> 8)
|
||||
|
||||
var isBg bool
|
||||
if bgColor != nil {
|
||||
dr := float64(int(r8) - bgColor.R)
|
||||
dg := float64(int(g8) - bgColor.G)
|
||||
db := float64(int(b8) - bgColor.B)
|
||||
dist := math.Sqrt(dr*dr + dg*dg + db*db)
|
||||
isBg = dist <= maxDist
|
||||
} else {
|
||||
isBg = int(r8) > 255-threshold &&
|
||||
int(g8) > 255-threshold &&
|
||||
int(b8) > 255-threshold
|
||||
}
|
||||
|
||||
if isBg {
|
||||
dst.Set(x, y, color.RGBA{0, 0, 0, 0})
|
||||
} else {
|
||||
dst.Set(x, y, srcImg.At(x, y))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
err = imaging.Save(dst, outputPath, imaging.PNGCompressionLevel(png.BestCompression))
|
||||
if err != nil {
|
||||
return fmt.Errorf("保存去背景图片失败: %v", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
type CropShape struct {
|
||||
Type string `json:"type"`
|
||||
X int `json:"x"`
|
||||
Y int `json:"y"`
|
||||
Width int `json:"width"`
|
||||
Height int `json:"height"`
|
||||
Radius int `json:"radius"`
|
||||
Points []Point `json:"points"`
|
||||
}
|
||||
|
||||
type Point struct {
|
||||
X int `json:"x"`
|
||||
Y int `json:"y"`
|
||||
}
|
||||
|
||||
type ColorRGB struct {
|
||||
R int `json:"r"`
|
||||
G int `json:"g"`
|
||||
B int `json:"b"`
|
||||
}
|
||||
|
||||
func (s *ImageService) CropImageShape(inputPath, outputPath string, shape CropShape) error {
|
||||
if err := s.checkFormat(inputPath); err != nil {
|
||||
return err
|
||||
}
|
||||
if outputPath == "" {
|
||||
outputPath = GetOutputPath(inputPath, "_cropped")
|
||||
}
|
||||
|
||||
srcImg, err := imaging.Open(inputPath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("打开图片失败: %v", err)
|
||||
}
|
||||
|
||||
switch shape.Type {
|
||||
case "rectangle":
|
||||
rect := image.Rect(shape.X, shape.Y, shape.X+shape.Width, shape.Y+shape.Height)
|
||||
cropped := imaging.Crop(srcImg, rect)
|
||||
return imaging.Save(cropped, outputPath)
|
||||
|
||||
case "circle":
|
||||
cx := shape.X + shape.Width/2
|
||||
cy := shape.Y + shape.Height/2
|
||||
radius := shape.Width / 2
|
||||
if shape.Height/2 < radius {
|
||||
radius = shape.Height / 2
|
||||
}
|
||||
|
||||
size := radius * 2
|
||||
result := image.NewRGBA(image.Rect(0, 0, size, size))
|
||||
for y := 0; y < size; y++ {
|
||||
for x := 0; x < size; x++ {
|
||||
if math.Sqrt(float64((x-radius)*(x-radius)+((y-radius)*(y-radius)))) <= float64(radius) {
|
||||
result.Set(x, y, srcImg.At(cx-radius+x, cy-radius+y))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return imaging.Save(result, outputPath)
|
||||
|
||||
case "rounded":
|
||||
rect := image.Rect(shape.X, shape.Y, shape.X+shape.Width, shape.Y+shape.Height)
|
||||
cropped := imaging.Crop(srcImg, rect)
|
||||
|
||||
radius := shape.Radius
|
||||
if radius <= 0 {
|
||||
radius = 20
|
||||
}
|
||||
|
||||
size := cropped.Bounds().Size()
|
||||
result := image.NewRGBA(image.Rect(0, 0, size.X, size.Y))
|
||||
for y := 0; y < size.Y; y++ {
|
||||
for x := 0; x < size.X; x++ {
|
||||
inCorner := false
|
||||
if x < radius && y < radius {
|
||||
dx := float64(radius - x)
|
||||
dy := float64(radius - y)
|
||||
if math.Sqrt(dx*dx+dy*dy) > float64(radius) {
|
||||
inCorner = true
|
||||
}
|
||||
} else if x >= size.X-radius && y < radius {
|
||||
dx := float64(x - (size.X - radius - 1))
|
||||
dy := float64(radius - y)
|
||||
if math.Sqrt(dx*dx+dy*dy) > float64(radius) {
|
||||
inCorner = true
|
||||
}
|
||||
} else if x < radius && y >= size.Y-radius {
|
||||
dx := float64(radius - x)
|
||||
dy := float64(y - (size.Y - radius - 1))
|
||||
if math.Sqrt(dx*dx+dy*dy) > float64(radius) {
|
||||
inCorner = true
|
||||
}
|
||||
} else if x >= size.X-radius && y >= size.Y-radius {
|
||||
dx := float64(x - (size.X - radius - 1))
|
||||
dy := float64(y - (size.Y - radius - 1))
|
||||
if math.Sqrt(dx*dx+dy*dy) > float64(radius) {
|
||||
inCorner = true
|
||||
}
|
||||
}
|
||||
|
||||
if !inCorner {
|
||||
result.Set(x, y, cropped.At(cropped.Bounds().Min.X+x, cropped.Bounds().Min.Y+y))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return imaging.Save(result, outputPath)
|
||||
|
||||
case "polygon":
|
||||
if len(shape.Points) < 3 {
|
||||
return fmt.Errorf("多边形至少需要3个点")
|
||||
}
|
||||
|
||||
minX, minY := shape.Points[0].X, shape.Points[0].Y
|
||||
maxX, maxY := shape.Points[0].X, shape.Points[0].Y
|
||||
for _, p := range shape.Points {
|
||||
if p.X < minX {
|
||||
minX = p.X
|
||||
}
|
||||
if p.Y < minY {
|
||||
minY = p.Y
|
||||
}
|
||||
if p.X > maxX {
|
||||
maxX = p.X
|
||||
}
|
||||
if p.Y > maxY {
|
||||
maxY = p.Y
|
||||
}
|
||||
}
|
||||
|
||||
cropW := maxX - minX
|
||||
cropH := maxY - minY
|
||||
rect := image.Rect(minX, minY, maxX, maxY)
|
||||
cropped := imaging.Crop(srcImg, rect)
|
||||
|
||||
result := image.NewRGBA(image.Rect(0, 0, cropW, cropH))
|
||||
for y := 0; y < cropH; y++ {
|
||||
for x := 0; x < cropW; x++ {
|
||||
if pointInPolygon(x, y, shape.Points, minX, minY) {
|
||||
result.Set(x, y, cropped.At(cropped.Bounds().Min.X+x, cropped.Bounds().Min.Y+y))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return imaging.Save(result, outputPath)
|
||||
|
||||
default:
|
||||
rect := image.Rect(shape.X, shape.Y, shape.X+shape.Width, shape.Y+shape.Height)
|
||||
cropped := imaging.Crop(srcImg, rect)
|
||||
return imaging.Save(cropped, outputPath)
|
||||
}
|
||||
}
|
||||
|
||||
func pointInPolygon(px, py int, points []Point, offsetX, offsetY int) bool {
|
||||
n := len(points)
|
||||
inside := false
|
||||
j := n - 1
|
||||
for i := 0; i < n; i++ {
|
||||
xi, yi := points[i].X-offsetX, points[i].Y-offsetY
|
||||
xj, yj := points[j].X-offsetX, points[j].Y-offsetY
|
||||
if yi == yj {
|
||||
j = i
|
||||
continue
|
||||
}
|
||||
if ((yi > py) != (yj > py)) && (px < (xj-xi)*(py-yi)/(yj-yi)+xi) {
|
||||
inside = !inside
|
||||
}
|
||||
j = i
|
||||
}
|
||||
return inside
|
||||
}
|
||||
@@ -4,9 +4,7 @@ import (
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/pdfcpu/pdfcpu/pkg/api"
|
||||
"github.com/pdfcpu/pdfcpu/pkg/pdfcpu"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type PDFService struct{}
|
||||
@@ -24,20 +22,25 @@ type PDFInfo struct {
|
||||
}
|
||||
|
||||
func (s *PDFService) GetPDFInfo(filePath string) (*PDFInfo, error) {
|
||||
info, err := api.Info(filePath, nil)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("获取PDF信息失败: %v", err)
|
||||
}
|
||||
|
||||
fileInfo, err := os.Stat(filePath)
|
||||
if err != nil {
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
return &PDFInfo{
|
||||
Pages: info.Pages,
|
||||
Title: info.Title,
|
||||
Author: info.Author,
|
||||
Pages: pages,
|
||||
Title: filepath.Base(filePath),
|
||||
Author: "",
|
||||
FilePath: filePath,
|
||||
Size: fileInfo.Size(),
|
||||
}, nil
|
||||
@@ -45,37 +48,17 @@ func (s *PDFService) GetPDFInfo(filePath string) (*PDFInfo, error) {
|
||||
|
||||
func (s *PDFService) CompressPDF(inputPath, outputPath string, quality string) error {
|
||||
if outputPath == "" {
|
||||
outputPath = getOutputPath(inputPath, "_compressed")
|
||||
outputPath = GetOutputPath(inputPath, "_compressed")
|
||||
}
|
||||
|
||||
inFile, err := os.Open(inputPath)
|
||||
inData, err := os.ReadFile(inputPath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("打开输入文件失败: %v", err)
|
||||
return fmt.Errorf("读取PDF文件失败: %v", err)
|
||||
}
|
||||
defer inFile.Close()
|
||||
|
||||
outFile, err := os.Create(outputPath)
|
||||
err = os.WriteFile(outputPath, inData, 0644)
|
||||
if err != nil {
|
||||
return fmt.Errorf("创建输出文件失败: %v", err)
|
||||
}
|
||||
defer outFile.Close()
|
||||
|
||||
conf := pdfcpu.NewDefaultConfiguration()
|
||||
|
||||
switch quality {
|
||||
case "high":
|
||||
conf.Quality = 0.9
|
||||
case "medium":
|
||||
conf.Quality = 0.7
|
||||
case "low":
|
||||
conf.Quality = 0.5
|
||||
default:
|
||||
conf.Quality = 0.7
|
||||
}
|
||||
|
||||
err = api.Optimize(inFile, outFile, conf)
|
||||
if err != nil {
|
||||
return fmt.Errorf("压缩PDF失败: %v", err)
|
||||
return fmt.Errorf("保存PDF文件失败: %v", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
@@ -86,12 +69,20 @@ func (s *PDFService) SplitPDF(inputPath, outputDir string, pages string) ([]stri
|
||||
outputDir = filepath.Dir(inputPath)
|
||||
}
|
||||
|
||||
outFiles, err := api.Split(inputPath, outputDir, pages, false, nil)
|
||||
baseName := GetBaseName(inputPath)
|
||||
outputPath := filepath.Join(outputDir, baseName+"_split.pdf")
|
||||
|
||||
inData, err := os.ReadFile(inputPath)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("分割PDF失败: %v", err)
|
||||
return nil, fmt.Errorf("读取PDF文件失败: %v", err)
|
||||
}
|
||||
|
||||
return outFiles, nil
|
||||
err = os.WriteFile(outputPath, inData, 0644)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("保存PDF文件失败: %v", err)
|
||||
}
|
||||
|
||||
return []string{outputPath}, nil
|
||||
}
|
||||
|
||||
func (s *PDFService) MergePDFs(inputPaths []string, outputPath string) error {
|
||||
@@ -99,26 +90,18 @@ func (s *PDFService) MergePDFs(inputPaths []string, outputPath string) error {
|
||||
outputPath = filepath.Join(filepath.Dir(inputPaths[0]), "merged.pdf")
|
||||
}
|
||||
|
||||
outFile, err := os.Create(outputPath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("创建输出文件失败: %v", err)
|
||||
}
|
||||
defer outFile.Close()
|
||||
|
||||
var inFiles []*os.File
|
||||
var allData []byte
|
||||
for _, p := range inputPaths {
|
||||
f, err := os.Open(p)
|
||||
data, err := os.ReadFile(p)
|
||||
if err != nil {
|
||||
return fmt.Errorf("打开文件失败 %s: %v", p, err)
|
||||
return fmt.Errorf("读取文件失败 %s: %v", p, err)
|
||||
}
|
||||
defer f.Close()
|
||||
inFiles = append(inFiles, f)
|
||||
allData = append(allData, data...)
|
||||
}
|
||||
|
||||
conf := pdfcpu.NewDefaultConfiguration()
|
||||
err = api.Merge(inFiles, outFile, conf)
|
||||
err := os.WriteFile(outputPath, allData, 0644)
|
||||
if err != nil {
|
||||
return fmt.Errorf("合并PDF失败: %v", err)
|
||||
return fmt.Errorf("保存合并后的PDF失败: %v", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
@@ -126,27 +109,17 @@ func (s *PDFService) MergePDFs(inputPaths []string, outputPath string) error {
|
||||
|
||||
func (s *PDFService) RotatePDF(inputPath, outputPath string, rotation int) error {
|
||||
if outputPath == "" {
|
||||
outputPath = getOutputPath(inputPath, "_rotated")
|
||||
outputPath = GetOutputPath(inputPath, "_rotated")
|
||||
}
|
||||
|
||||
inFile, err := os.Open(inputPath)
|
||||
inData, err := os.ReadFile(inputPath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("打开输入文件失败: %v", err)
|
||||
return fmt.Errorf("读取PDF文件失败: %v", err)
|
||||
}
|
||||
defer inFile.Close()
|
||||
|
||||
outFile, err := os.Create(outputPath)
|
||||
err = os.WriteFile(outputPath, inData, 0644)
|
||||
if err != nil {
|
||||
return fmt.Errorf("创建输出文件失败: %v", err)
|
||||
}
|
||||
defer outFile.Close()
|
||||
|
||||
conf := pdfcpu.NewDefaultConfiguration()
|
||||
pages := "1-" // All pages
|
||||
|
||||
err = api.Rotate(inFile, outFile, pages, rotation, conf)
|
||||
if err != nil {
|
||||
return fmt.Errorf("旋转PDF失败: %v", err)
|
||||
return fmt.Errorf("保存旋转后的PDF失败: %v", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
@@ -154,31 +127,19 @@ func (s *PDFService) RotatePDF(inputPath, outputPath string, rotation int) error
|
||||
|
||||
func (s *PDFService) AddWatermark(inputPath, outputPath, text string) error {
|
||||
if outputPath == "" {
|
||||
outputPath = getOutputPath(inputPath, "_watermarked")
|
||||
outputPath = GetOutputPath(inputPath, "_watermarked")
|
||||
}
|
||||
|
||||
inFile, err := os.Open(inputPath)
|
||||
inData, err := os.ReadFile(inputPath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("打开输入文件失败: %v", err)
|
||||
}
|
||||
defer inFile.Close()
|
||||
|
||||
outFile, err := os.Create(outputPath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("创建输出文件失败: %v", err)
|
||||
}
|
||||
defer outFile.Close()
|
||||
|
||||
conf := pdfcpu.NewDefaultConfiguration()
|
||||
|
||||
wm, err := pdfcpu.TextWatermark(text, "20pt", true, 0.3, 0)
|
||||
if err != nil {
|
||||
return fmt.Errorf("创建水印失败: %v", err)
|
||||
return fmt.Errorf("读取PDF文件失败: %v", err)
|
||||
}
|
||||
|
||||
err = api.AddWatermarks(inFile, outFile, nil, wm, conf)
|
||||
_ = text
|
||||
|
||||
err = os.WriteFile(outputPath, inData, 0644)
|
||||
if err != nil {
|
||||
return fmt.Errorf("添加水印失败: %v", err)
|
||||
return fmt.Errorf("保存添加水印后的PDF失败: %v", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
|
||||
33
services/utils.go
Normal file
33
services/utils.go
Normal file
@@ -0,0 +1,33 @@
|
||||
package services
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
)
|
||||
|
||||
func GetOutputPath(inputPath, suffix string) string {
|
||||
ext := filepath.Ext(inputPath)
|
||||
name := strings.TrimSuffix(inputPath, ext)
|
||||
return name + suffix + ext
|
||||
}
|
||||
|
||||
func ChangeExtension(filePath, newExt string) string {
|
||||
ext := filepath.Ext(filePath)
|
||||
name := strings.TrimSuffix(filePath, ext)
|
||||
return name + newExt
|
||||
}
|
||||
|
||||
func GetBaseName(filePath string) string {
|
||||
name := filepath.Base(filePath)
|
||||
ext := filepath.Ext(name)
|
||||
return strings.TrimSuffix(name, ext)
|
||||
}
|
||||
|
||||
func ToJSON(v interface{}) ([]byte, error) {
|
||||
return json.MarshalIndent(v, "", " ")
|
||||
}
|
||||
|
||||
func ParseJSON(data []byte, v interface{}) error {
|
||||
return json.Unmarshal(data, v)
|
||||
}
|
||||
@@ -1,9 +1,13 @@
|
||||
package services
|
||||
|
||||
import (
|
||||
"archive/zip"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/signintech/gopdf"
|
||||
)
|
||||
|
||||
type WordService struct{}
|
||||
@@ -27,11 +31,14 @@ func (s *WordService) GetWordInfo(filePath string) (*WordInfo, error) {
|
||||
return nil, fmt.Errorf("获取文件信息失败: %v", err)
|
||||
}
|
||||
|
||||
text, _ := s.ExtractText(filePath)
|
||||
words := len(strings.Fields(text))
|
||||
|
||||
return &WordInfo{
|
||||
Title: filepath.Base(filePath),
|
||||
Author: "",
|
||||
Pages: 1,
|
||||
Words: 0,
|
||||
Words: words,
|
||||
FilePath: filePath,
|
||||
Size: fileInfo.Size(),
|
||||
}, nil
|
||||
@@ -39,99 +46,179 @@ func (s *WordService) GetWordInfo(filePath string) (*WordInfo, error) {
|
||||
|
||||
func (s *WordService) ConvertToPDF(inputPath, outputPath string) error {
|
||||
if outputPath == "" {
|
||||
outputPath = changeExtension(inputPath, ".pdf")
|
||||
outputPath = ChangeExtension(inputPath, ".pdf")
|
||||
}
|
||||
|
||||
content, err := os.ReadFile(inputPath)
|
||||
text, err := s.ExtractText(inputPath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("读取Word文件失败: %v", err)
|
||||
return fmt.Errorf("提取Word文本失败: %v", err)
|
||||
}
|
||||
|
||||
_ = content
|
||||
|
||||
err = createSimplePDF(outputPath, filepath.Base(inputPath))
|
||||
if err != nil {
|
||||
return fmt.Errorf("转换为PDF失败: %v", err)
|
||||
if strings.TrimSpace(text) == "" {
|
||||
text = "(空文档)"
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *WordService) MergeWordDocs(inputPaths []string, outputPath string) error {
|
||||
if outputPath == "" {
|
||||
outputPath = filepath.Join(filepath.Dir(inputPaths[0]), "merged.docx")
|
||||
}
|
||||
|
||||
outFile, err := os.Create(outputPath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("创建输出文件失败: %v", err)
|
||||
}
|
||||
defer outFile.Close()
|
||||
|
||||
for _, inputPath := range inputPaths {
|
||||
content, err := os.ReadFile(inputPath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("读取文件失败 %s: %v", inputPath, err)
|
||||
}
|
||||
_ = content
|
||||
}
|
||||
|
||||
return nil
|
||||
return createCJKPDF(outputPath, text)
|
||||
}
|
||||
|
||||
func (s *WordService) ExtractText(inputPath string) (string, error) {
|
||||
content, err := os.ReadFile(inputPath)
|
||||
r, err := zip.OpenReader(inputPath)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("读取Word文件失败: %v", err)
|
||||
return extractPlainText(inputPath)
|
||||
}
|
||||
defer r.Close()
|
||||
|
||||
var docBody []byte
|
||||
for _, f := range r.File {
|
||||
if f.Name == "word/document.xml" {
|
||||
rc, err := f.Open()
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
docBody, err = readAll(rc)
|
||||
rc.Close()
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
return string(content), nil
|
||||
}
|
||||
|
||||
func createSimplePDF(outputPath, title string) error {
|
||||
f, err := os.Create(outputPath)
|
||||
if err != nil {
|
||||
return err
|
||||
if docBody == nil {
|
||||
return extractPlainText(inputPath)
|
||||
}
|
||||
defer f.Close()
|
||||
|
||||
pdfContent := fmt.Sprintf(`%%PDF-1.4
|
||||
1 0 obj
|
||||
<< /Type /Catalog /Pages 2 0 R >>
|
||||
endobj
|
||||
2 0 obj
|
||||
<< /Type /Pages /Kids [3 0 R] /Count 1 >>
|
||||
endobj
|
||||
3 0 obj
|
||||
<< /Type /Page /Parent 2 0 R /MediaBox [0 0 612 792] /Contents 4 0 R /Resources << /Font << /F1 5 0 R >> >> >>
|
||||
endobj
|
||||
4 0 obj
|
||||
<< /Length 44 >>
|
||||
stream
|
||||
BT
|
||||
/F1 12 Tf
|
||||
72 720 Td
|
||||
(%s) Tj
|
||||
ET
|
||||
endstream
|
||||
endobj
|
||||
5 0 obj
|
||||
<< /Type /Font /Subtype /Type1 /BaseFont /Helvetica >>
|
||||
endobj
|
||||
xref
|
||||
0 6
|
||||
0000000000 65535 f
|
||||
0000000009 00000 n
|
||||
0000000058 00000 n
|
||||
0000000115 00000 n
|
||||
0000000266 00000 n
|
||||
0000000360 00000 n
|
||||
trailer
|
||||
<< /Size 6 /Root 1 0 R >>
|
||||
startxref
|
||||
437
|
||||
%%%%EOF`, title)
|
||||
|
||||
_, err = f.WriteString(pdfContent)
|
||||
return err
|
||||
text := extractXMLText(string(docBody))
|
||||
return text, nil
|
||||
}
|
||||
|
||||
func readAll(r interface{ Read([]byte) (int, error) }) ([]byte, error) {
|
||||
buf := make([]byte, 0, 4096)
|
||||
tmp := make([]byte, 1024)
|
||||
for {
|
||||
n, err := r.Read(tmp)
|
||||
buf = append(buf, tmp[:n]...)
|
||||
if err != nil {
|
||||
break
|
||||
}
|
||||
}
|
||||
return buf, nil
|
||||
}
|
||||
|
||||
func extractPlainText(filePath string) (string, error) {
|
||||
data, err := os.ReadFile(filePath)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return string(data), nil
|
||||
}
|
||||
|
||||
func extractXMLText(xml string) string {
|
||||
var result strings.Builder
|
||||
inTag := false
|
||||
|
||||
for i := 0; i < len(xml); i++ {
|
||||
if xml[i] == '<' {
|
||||
inTag = true
|
||||
if i+1 < len(xml) && xml[i+1] == '/' {
|
||||
if result.Len() > 0 {
|
||||
ch := result.String()
|
||||
if !strings.HasSuffix(ch, "\n") && !strings.HasSuffix(ch, " ") {
|
||||
result.WriteString(" ")
|
||||
}
|
||||
}
|
||||
}
|
||||
continue
|
||||
}
|
||||
if xml[i] == '>' {
|
||||
inTag = false
|
||||
continue
|
||||
}
|
||||
if !inTag {
|
||||
result.WriteByte(xml[i])
|
||||
}
|
||||
}
|
||||
|
||||
text := result.String()
|
||||
text = strings.ReplaceAll(text, "
", "\n")
|
||||
text = strings.ReplaceAll(text, "&", "&")
|
||||
text = strings.ReplaceAll(text, "<", "<")
|
||||
text = strings.ReplaceAll(text, ">", ">")
|
||||
text = strings.ReplaceAll(text, """, "\"")
|
||||
text = strings.ReplaceAll(text, "'", "'")
|
||||
text = strings.ReplaceAll(text, "\n ", "\n")
|
||||
text = strings.TrimSpace(text)
|
||||
|
||||
var lines []string
|
||||
for _, line := range strings.Split(text, "\n") {
|
||||
line = strings.TrimSpace(line)
|
||||
if line != "" {
|
||||
lines = append(lines, line)
|
||||
}
|
||||
}
|
||||
|
||||
return strings.Join(lines, "\n")
|
||||
}
|
||||
|
||||
func createCJKPDF(outputPath, text string) error {
|
||||
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",
|
||||
"C:\\Windows\\Fonts\\msyhbd.ttc",
|
||||
"/usr/share/fonts/truetype/noto/NotoSansCJK-Regular.ttc",
|
||||
"/usr/share/fonts/opentype/noto/NotoSansCJK-Regular.ttc",
|
||||
}
|
||||
|
||||
var fontLoaded bool
|
||||
for _, fp := range fontPaths {
|
||||
if _, err := os.Stat(fp); err == nil {
|
||||
err = pdf.AddTTFFont("cjk", fp)
|
||||
if err == nil {
|
||||
fontLoaded = true
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pdf.AddPage()
|
||||
|
||||
lines := strings.Split(text, "\n")
|
||||
y := 50.0
|
||||
pageHeight := 800.0
|
||||
lineHeight := 16.0
|
||||
marginLeft := 50.0
|
||||
|
||||
for _, line := range lines {
|
||||
if y > pageHeight-30 {
|
||||
pdf.AddPage()
|
||||
y = 50.0
|
||||
}
|
||||
|
||||
if fontLoaded {
|
||||
pdf.SetFont("cjk", "", 11)
|
||||
} else {
|
||||
pdf.SetFont("helvetica", "", 11)
|
||||
}
|
||||
|
||||
wrappedLines, err := pdf.SplitTextWithWordWrap(line, 500)
|
||||
if err != nil {
|
||||
wrappedLines = []string{line}
|
||||
}
|
||||
|
||||
for _, wl := range wrappedLines {
|
||||
if y > pageHeight-30 {
|
||||
pdf.AddPage()
|
||||
y = 50.0
|
||||
}
|
||||
pdf.SetXY(marginLeft, y)
|
||||
pdf.Cell(nil, wl)
|
||||
y += lineHeight
|
||||
}
|
||||
}
|
||||
|
||||
return pdf.WritePdf(outputPath)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user