初始化

This commit is contained in:
李琦
2026-06-17 08:05:20 +08:00
parent 6dfe73191c
commit fb3d707a32
36 changed files with 7002 additions and 261 deletions

261
services/excel_service.go Normal file
View 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
View 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
}

View File

@@ -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
View 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)
}

View File

@@ -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, "&#xA;", "\n")
text = strings.ReplaceAll(text, "&amp;", "&")
text = strings.ReplaceAll(text, "&lt;", "<")
text = strings.ReplaceAll(text, "&gt;", ">")
text = strings.ReplaceAll(text, "&quot;", "\"")
text = strings.ReplaceAll(text, "&#39;", "'")
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)
}