圖片寫入pdf檔案

charliecen發表於2022-06-14

圖片寫入pdf檔案

需求: 要求圖片自適應大小,居中寫入pdf檔案中,兩張小圖片放入一張A4紙,大圖片寫入一張A4紙

圖片上傳

// 判斷路徑是否存在
func Exists(path string) bool {
    _, err := os.Stat(path) //os.Stat獲取檔案資訊
    if err != nil {
        if os.IsExist(err) {
            return true
        }
        return false
    }
    return true
}

// UploadFiles 上傳多個檔案
func UploadFiles(r *http.Request, key, filePath string, isCreateDir bool) ([]string, error) {
    var (
        newFilePath = filePath
        result      []string
    )
    // 建立目錄
    if isCreateDir {
        ym := time.Now().Format("200601")
        newFilePath = filePath + ym + "/"
    }
    if flag := Exists(newFilePath); !flag {
        err := os.MkdirAll(newFilePath, 0755)
        if err != nil {
            return result, errors.Wrapf(xerr.NewErrCode(xerr.CreateDirFailed), "tool UploadFile create make dir failed  err:%v", err)
        }
    }
    // 根據欄位名獲取表單檔案
    err := r.ParseMultipartForm(1024 * 1024 * 1024)
    if err != nil {
        return nil, errors.Wrapf(xerr.NewErrCode(xerr.UploadFileExceededLimit), "tool UploadFile upload image size exteed limit  err:%v", err)
    }
    files := r.MultipartForm.File[key]
    // 最多上傳9張
    if len(files) > 9 {
        return nil, errors.Wrapf(xerr.NewErrCode(xerr.UploadFilesCountOverLimit), "tool UploadFile upload image count over limit  err:%v", err)
    }
    for _, file := range files {
        saveFile, err := SaveFile(file, newFilePath)
        if err != nil {
            return result, err
        }
        result = append(result, saveFile)
    }

    return result, nil
}

// SaveFile 儲存單個檔案
func SaveFile(file *multipart.FileHeader, filePath string) (string, error) {
    if flag := checkImageSuffix(file.Filename); !flag {
        return "", errors.Wrapf(xerr.NewErrCode(xerr.UploadImageSuffixError), "tool SaveFile check image suffix invalid")
    }
    // 建立新的檔名
    newFileName := CreateTimeWithFileName(file)
    // 建立儲存檔案
    destFile, err := os.Create(filePath + newFileName)
    if err != nil {
        return "", errors.Wrapf(xerr.NewErrCode(xerr.CreateSaveFileFailed), "tool SaveFile create dest file failed, err:%v", err)
    }
    openFile, err := file.Open()
    if err != nil {
        return "", errors.Wrapf(xerr.NewErrCode(xerr.OpenImageFileFailed), "tool SaveFile open upload image file failed, err:%v", err)
    }
    defer openFile.Close()

    // 讀取表單檔案,寫入儲存檔案
    _, err = io.Copy(destFile, openFile)
    if err != nil {
        return "", errors.Wrapf(xerr.NewErrCode(xerr.SaveFileFailed), "tool SaveFile save to dest file failed,  err:%v", err)
    }
    // 關閉後才能修改檔名
    destFile.Close()
    newFilePath, err := GetRealFilePath(filePath + newFileName)
    if err != nil {
        return "", errors.Wrapf(xerr.NewErrCode(xerr.ChangeImageRealSuffixFailed), "tool SaveFile change real suffix failed,  err:%v", err)
    }
    return newFilePath, nil
}

// 檢測檔案字尾
func checkImageSuffix(filePath string) bool {
    fileSuffix := path.Ext(filePath)
    if fileSuffix == ".jpeg" || fileSuffix == ".jpg" || fileSuffix == ".gif" || fileSuffix == ".png" {
        return true
    } else {
        return false
    }
}


// CreateTimeWithFileName 建立時間與檔名合併的檔名
func CreateTimeWithFileName(file *multipart.FileHeader) string {
    fileName := file.Filename
    // 毫秒
    now := time.Now().UnixNano() / 1e6
    fileSuffix := path.Ext(fileName)
    filePrefix := fileName[0 : len(fileName)-len(fileSuffix)]
    newFileName := fmt.Sprintf("%s_%v%s", filePrefix, now, fileSuffix)
    return newFileName
}

// 獲取真實檔案的路徑
func GetRealFilePath(filePath string) (string, error) {
    imageType, err := GetImageRealType(filePath)
    if err != nil {
        return "", err
    }
    paths, fileName := filepath.Split(filePath)

    fileSuffix := path.Ext(fileName)
    filePrefix := fileName[0 : len(fileName)-len(fileSuffix)]
    if fileSuffix != imageType {
        newFileName := fmt.Sprintf("%s%s", filePrefix, imageType)
        newFilePath := fmt.Sprintf("%s%s", paths, newFileName)
        err := os.Rename(filePath, newFilePath)
        if err != nil {
            return "", err
        }
        return newFilePath, nil
    }
    return filePath, nil
}

// 獲取檔案的真實字尾
func GetImageRealType(filePath string) (string, error) {
    file, err := os.Open(filePath)

    if err != nil {
        return "", err
    }
    defer file.Close()
    buff := make([]byte, 512)
    _, err = file.Read(buff)

    if err != nil {
        return "", err
    }

    filetype := http.DetectContentType(buff)

    switch filetype {
    case "image/jpeg", "image/jpg":
        return ".jpg", nil

    case "image/gif":
        return ".gif", nil

    case "image/png":
        return ".png", nil
    default:
        return "", errors.New("檔案不是圖片型別")
    }
}

寫入PDF

首先匯入第三方庫

go get github.com/jung-kurt/gofpdf

其次,把上傳的圖片寫入到pdf

// 寫入pdf
func (l *ImageWritePDFLogic) scanImageWritePdf(filePaths []string) (string, error) {
    if len(filePaths) == 0 {
        return "", errors.Wrapf(xerr.NewErrCode(xerr.ScanFilePathsIsEmpty), "rpc scanImageWritePdf upload files is empty, data:%+v", filePaths)
    }
    saveMd5FileName := tool.Md5(filePaths[0]) + ".pdf"
    saveMd5FilePath := path.Dir(filePaths[0]) + "/" + saveMd5FileName
    var opt gofpdf.ImageOptions
    pdf := gofpdf.New("P", "mm", "A4", "")
    // 字型路徑
    fontPath := l.svcCtx.Config.Upload.FontPath
    //fontFullPath := fontPath + "NotoSansSC-Regular.ttf"
    fontFullPath := fontPath + "simfang.ttf"
    if ok := tool.Exists(fontFullPath); !ok {
        return "", errors.Wrapf(xerr.NewErrCode(xerr.FontFileNotExists), "rpc scanImageWritePdf font path is not Exists, data:%+v", fontFullPath)
    }
    // 針對linux 系統字型問題
    fontBytes, _ := ioutil.ReadFile(fontFullPath)
    pdf.AddUTF8FontFromBytes("simfang", "", fontBytes)
    // windows 適用,但是linux 不適用,註釋掉
    //pdf.AddUTF8Font("simfang", "", fontFullPath)
    pdf.SetFont("simfang", "", 11)
    pdf.SetX(60)
    // 圖片型別
    //opt.ImageType = "png"

    //設定頁尾
    pdf.SetFooterFunc(func() {
        pdf.SetY(-10)
        pdf.CellFormat(
            0, 10,
            fmt.Sprintf("當前第 %d 頁,共 {nb} 頁", pdf.PageNo()), //字串中的 {nb}。大括號是可以省的,但不建議這麼做
            "", 0, "C", false, 0, "",
        )
    })
    //給個空字串就會去替換預設的 "{nb}"。
    //如果這裡指定了特別的字串,那麼SetFooterFunc() 中的 "nb" 也必須換成這個特別的字串
    pdf.AliasNbPages("")

    // 每張紙最多放兩張圖片,  1<=i<=2
    i := 1
    // 判斷圖片大小
    for _, filePath := range filePaths {
        // 判斷檔案是否存在
        if ok := tool.Exists(filePath); !ok {
            return "", errors.Wrapf(xerr.NewErrCode(xerr.FindUploadFileFailed), "rpc scanImageWritePdf file upload is not Exists, data:%+v", filePath)
        }

        // 重置圖片大小
        twh, err := tool.MakeThumbnailWeightHeight(filePath)
        if err != nil {
            return "", errors.Wrapf(xerr.NewErrCode(xerr.ThumbnailImageFailed), "rpc scanImageWritePdf make image width :%+v height: %+v, err:%v", twh.Width, twh.Height, err)
        }
        // 獲取圖片的位置
        a4p := tool.GetImagePositionWithA4(twh, i)
        // 單張
        if twh.Single {
            logx.Infof("單張存放, i:%v", i)
            pdf.AddPage()
            // 圖片設定
            pdf.ImageOptions(filePath, a4p.Width, a4p.Height, twh.Width, twh.Height, false, opt, 0, "")
            i = 1
        } else {
            // 兩張圖片放一張A4
            // 放第一張圖片,則新建紙張
            if i == 1 {
                logx.Infof("兩張存放 pdf.AddPage(), i:%v", i)
                pdf.AddPage()
                i++ // 第二個位置
            } else {
                logx.Infof("兩張存放, i:%v", i)
                i-- // 第二個位置,減一,變成第一個位置
            }

            // 圖片設定
            pdf.ImageOptions(filePath, a4p.Width, a4p.Height, twh.Width, twh.Height, false, opt, 0, "")
        }

    }

    // 儲存pdf檔案
    if err := pdf.OutputFileAndClose(saveMd5FilePath); err != nil {
        return "", errors.Wrapf(xerr.NewErrCode(xerr.ImageWritePDFFailed), "rpc scanImageWritePdf save to pdf file failed, err:%v, data:%+v", err, saveMd5FilePath)
    }

    ym := time.Now().Format("200601")
    url := l.svcCtx.Config.Upload.Url + saveMd5FileName
    if strings.Contains(filePaths[0], ym) {
        url = l.svcCtx.Config.Upload.Url + ym + "/" + saveMd5FileName
    }
    return url, nil
}

圖片縮放和位置定位

// 定義A4紙張最大的大小
const DEFAULT_MAX_WIDTH float64 = 210
const DEFAULT_MAX_HEIGHT float64 = 297

type ThumbnailWeightHeight struct {
    Width  float64
    Height float64
    Single bool
}

// MakeThumbnailWeightHeight 重置圖片的大小
//  1 英寸 = 2.54 釐米,解析度 = 96 畫素/英寸 = 96 畫素/2.54 釐米,因此 1 畫素 = 2.54 釐米/96 = 0.02645833333 釐米。
// A4 大小 210 x 297 cm
func MakeThumbnailWeightHeight(imagePath string) (*ThumbnailWeightHeight, error) {
    var twh = new(ThumbnailWeightHeight)
    file, _ := os.Open(imagePath)
    defer file.Close()

    img, _, err := image.Decode(file)
    if err != nil {
        return nil, errors.Wrapf(xerr.NewErrCode(xerr.GetImageSizeFailed), "utils MakeThumbnailWeightHeight, data:%+v", imagePath)
    }

    b := img.Bounds()
    twh.Width = float64(b.Max.X)  // 圖片的寬度, 畫素
    twh.Height = float64(b.Max.Y) // 圖片的高度, 畫素
    twh.Single = false            // 一張A4紙可以放置兩張圖片
    //logx.Infof("圖片寬度:%v, 高度:%v", twh.Width, twh.Height)

    // 計算出圖片的cm
    twh.Width = math.Round(twh.Width * 0.2645833333)
    twh.Height = math.Round(twh.Height * 0.2645833333)
    //logx.Infof("計算圖片大小cm,圖片寬度:%v, 高度:%v", twh.Width, twh.Height)

    // 大於限制,重置大小
    for twh.Width >= DEFAULT_MAX_WIDTH || twh.Height >= DEFAULT_MAX_HEIGHT {
        twh.Width /= 2
        twh.Height /= 2
        //logx.Infof("重置後,圖片寬度:%v, 高度:%v", twh.Width, twh.Height)
    }

    // 高度大於等於A4高度的一半,則只能放置一張圖片
    if twh.Height >= DEFAULT_MAX_HEIGHT/2 {
        twh.Single = true
    }

    return twh, nil
}


type A4PositionWightHeight struct {
    Width  float64
    Height float64
}

// GetImagePositionWithA4 獲取圖片在A4紙的位置
func GetImagePositionWithA4(twh *ThumbnailWeightHeight, i int) *A4PositionWightHeight {
    var a4p = new(A4PositionWightHeight)
    a4p.Width = (DEFAULT_MAX_WIDTH / 2) - (twh.Width / 2)
    // 單張
    if twh.Single {
        a4p.Height = (DEFAULT_MAX_HEIGHT - twh.Height) / 2
    } else {
        // 第一張的高度位置
        if i == 1 {
            a4p.Height = ((DEFAULT_MAX_HEIGHT / 2) / 2) - (twh.Height / 2)
        } else {
            // 第二張的高度位置
            a4p.Height = (DEFAULT_MAX_HEIGHT / 2) + ((DEFAULT_MAX_HEIGHT / 2) / 2) - (twh.Height / 2)
        }
    }

    // logx.Infof("計算圖片大小cm,圖片寬度:%v, 高度1:%v, 高度2:%v", a4p.Width, a4p.FirstHeight, a4p.SecondHeight)
    return a4p
}

測試

需要把上傳的目錄支援nginx能訪問, 這樣使用域名可以訪問到

curl --location --request POST --X POST 'http://localhost/look/image_share' \
--header 'User-Agent: Apipost client Runtime/+https://www.apipost.cn/' \
--form 'image=@/Users/charlie/Downloads/12.png' \
--form 'image=@/Users/charlie/Downloads/11.png' \
--form 'image=@/Users/charlie/Downloads/13.png' \
--form 'image=@/Users/charlie/Downloads/14.png'


{
    "code": 200,
    "msg": "OK",
    "data": {
        "url": "http://localhsot/upload/202206/6d6d85f74723030d0113e64c7235d4d4.pdf"
    }
}

本作品採用《CC 協議》,轉載必須註明作者和本文連結

相關文章