v0.0.451 wpdf image processing
Some checks failed
Build Docker and Deploy / Run goext test-suite (push) Has been cancelled
Some checks failed
Build Docker and Deploy / Run goext test-suite (push) Has been cancelled
This commit is contained in:
parent
c28bc086b2
commit
246e555f3f
@ -69,6 +69,9 @@ var (
|
||||
TypeUnauthorized = NewType("UNAUTHORIZED", langext.Ptr(401))
|
||||
TypeAuthFailed = NewType("AUTH_FAILED", langext.Ptr(401))
|
||||
|
||||
TypeInvalidImage = NewType("IMAGEEXT_INVALID_IMAGE", langext.Ptr(400))
|
||||
TypeInvalidMimeType = NewType("IMAGEEXT_INVALID_MIMETYPE", langext.Ptr(400))
|
||||
|
||||
// other values come from the downstream application that uses goext
|
||||
)
|
||||
|
||||
|
2
go.mod
2
go.mod
@ -15,6 +15,7 @@ require (
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/disintegration/imaging v1.6.2
|
||||
github.com/jung-kurt/gofpdf v1.16.2
|
||||
golang.org/x/sync v0.7.0
|
||||
)
|
||||
@ -53,6 +54,7 @@ require (
|
||||
github.com/xdg-go/stringprep v1.0.4 // indirect
|
||||
github.com/youmark/pkcs8 v0.0.0-20240424034433-3c2c7870ae76 // indirect
|
||||
golang.org/x/arch v0.8.0 // indirect
|
||||
golang.org/x/image v0.16.0 // indirect
|
||||
golang.org/x/net v0.25.0 // indirect
|
||||
golang.org/x/text v0.15.0 // indirect
|
||||
google.golang.org/protobuf v1.34.1 // indirect
|
||||
|
6
go.sum
6
go.sum
@ -43,6 +43,8 @@ github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSV
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
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/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
|
||||
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
|
||||
github.com/gabriel-vasile/mimetype v1.4.3 h1:in2uUcidCuFcDKtdcBxlR0rJ1+fsokWf+uqxgUFjbI0=
|
||||
@ -222,6 +224,10 @@ golang.org/x/crypto v0.22.0/go.mod h1:vr6Su+7cTlO45qkww3VDJlzDn0ctJvRgYbC2NvXHt+
|
||||
golang.org/x/crypto v0.23.0 h1:dIJU/v2J8Mdglj/8rJ6UUOM3Zc9zLZxVZwwxMooUSAI=
|
||||
golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8=
|
||||
golang.org/x/image v0.0.0-20190910094157-69e4b8554b2a/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
|
||||
golang.org/x/image v0.0.0-20191009234506-e7c1f5e7dbb8 h1:hVwzHzIUGRjiF7EcUjqNxk3NCfkPxbDKRdnNE1Rpg0U=
|
||||
golang.org/x/image v0.0.0-20191009234506-e7c1f5e7dbb8/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
|
||||
golang.org/x/image v0.16.0 h1:9kloLAKhUufZhA12l5fwnx2NZW39/we1UhBesW433jw=
|
||||
golang.org/x/image v0.16.0/go.mod h1:ugSZItdV4nOxyqp56HmXwH0Ry0nBCpjnZdpDaIHdoPs=
|
||||
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
|
||||
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
|
@ -1,5 +1,5 @@
|
||||
package goext
|
||||
|
||||
const GoextVersion = "0.0.450"
|
||||
const GoextVersion = "0.0.451"
|
||||
|
||||
const GoextVersionTimestamp = "2024-05-14T11:52:56+0200"
|
||||
const GoextVersionTimestamp = "2024-05-14T12:46:49+0200"
|
||||
|
3
imageext/enums.go
Normal file
3
imageext/enums.go
Normal file
@ -0,0 +1,3 @@
|
||||
package imageext
|
||||
|
||||
//go:generate go run ../_gen/enum-generate.go -- enums_gen.go
|
300
imageext/enums_gen.go
Normal file
300
imageext/enums_gen.go
Normal file
@ -0,0 +1,300 @@
|
||||
// Code generated by enum-generate.go DO NOT EDIT.
|
||||
|
||||
package imageext
|
||||
|
||||
import "gogs.mikescher.com/BlackForestBytes/goext/langext"
|
||||
import "gogs.mikescher.com/BlackForestBytes/goext/enums"
|
||||
|
||||
const ChecksumEnumGenerator = "1da5383c33ee442fd0b899369053f66bdc85bed2dbf906949d3edfeedfe13340" // GoExtVersion: 0.0.449
|
||||
|
||||
// ================================ ImageFit ================================
|
||||
//
|
||||
// File: image.go
|
||||
// StringEnum: true
|
||||
// DescrEnum: false
|
||||
// DataEnum: false
|
||||
//
|
||||
|
||||
var __ImageFitValues = []ImageFit{
|
||||
ImageFitStretch,
|
||||
ImageFitCover,
|
||||
ImageFitContainCenter,
|
||||
ImageFitContainTopLeft,
|
||||
ImageFitContainTopRight,
|
||||
ImageFitContainBottomLeft,
|
||||
ImageFitContainBottomRight,
|
||||
}
|
||||
|
||||
var __ImageFitVarnames = map[ImageFit]string{
|
||||
ImageFitStretch: "ImageFitStretch",
|
||||
ImageFitCover: "ImageFitCover",
|
||||
ImageFitContainCenter: "ImageFitContainCenter",
|
||||
ImageFitContainTopLeft: "ImageFitContainTopLeft",
|
||||
ImageFitContainTopRight: "ImageFitContainTopRight",
|
||||
ImageFitContainBottomLeft: "ImageFitContainBottomLeft",
|
||||
ImageFitContainBottomRight: "ImageFitContainBottomRight",
|
||||
}
|
||||
|
||||
func (e ImageFit) Valid() bool {
|
||||
return langext.InArray(e, __ImageFitValues)
|
||||
}
|
||||
|
||||
func (e ImageFit) Values() []ImageFit {
|
||||
return __ImageFitValues
|
||||
}
|
||||
|
||||
func (e ImageFit) ValuesAny() []any {
|
||||
return langext.ArrCastToAny(__ImageFitValues)
|
||||
}
|
||||
|
||||
func (e ImageFit) ValuesMeta() []enums.EnumMetaValue {
|
||||
return ImageFitValuesMeta()
|
||||
}
|
||||
|
||||
func (e ImageFit) String() string {
|
||||
return string(e)
|
||||
}
|
||||
|
||||
func (e ImageFit) VarName() string {
|
||||
if d, ok := __ImageFitVarnames[e]; ok {
|
||||
return d
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (e ImageFit) TypeName() string {
|
||||
return "ImageFit"
|
||||
}
|
||||
|
||||
func (e ImageFit) PackageName() string {
|
||||
return "media"
|
||||
}
|
||||
|
||||
func (e ImageFit) Meta() enums.EnumMetaValue {
|
||||
return enums.EnumMetaValue{VarName: e.VarName(), Value: e, Description: nil}
|
||||
}
|
||||
|
||||
func ParseImageFit(vv string) (ImageFit, bool) {
|
||||
for _, ev := range __ImageFitValues {
|
||||
if string(ev) == vv {
|
||||
return ev, true
|
||||
}
|
||||
}
|
||||
return "", false
|
||||
}
|
||||
|
||||
func ImageFitValues() []ImageFit {
|
||||
return __ImageFitValues
|
||||
}
|
||||
|
||||
func ImageFitValuesMeta() []enums.EnumMetaValue {
|
||||
return []enums.EnumMetaValue{
|
||||
ImageFitStretch.Meta(),
|
||||
ImageFitCover.Meta(),
|
||||
ImageFitContainCenter.Meta(),
|
||||
ImageFitContainTopLeft.Meta(),
|
||||
ImageFitContainTopRight.Meta(),
|
||||
ImageFitContainBottomLeft.Meta(),
|
||||
ImageFitContainBottomRight.Meta(),
|
||||
}
|
||||
}
|
||||
|
||||
// ================================ ImageCompresson ================================
|
||||
//
|
||||
// File: image.go
|
||||
// StringEnum: true
|
||||
// DescrEnum: false
|
||||
// DataEnum: false
|
||||
//
|
||||
|
||||
var __ImageCompressonValues = []ImageCompresson{
|
||||
CompressionPNGNone,
|
||||
CompressionPNGSpeed,
|
||||
CompressionPNGBest,
|
||||
CompressionJPEG100,
|
||||
CompressionJPEG90,
|
||||
CompressionJPEG80,
|
||||
CompressionJPEG70,
|
||||
CompressionJPEG60,
|
||||
CompressionJPEG50,
|
||||
CompressionJPEG25,
|
||||
CompressionJPEG10,
|
||||
CompressionJPEG1,
|
||||
}
|
||||
|
||||
var __ImageCompressonVarnames = map[ImageCompresson]string{
|
||||
CompressionPNGNone: "CompressionPNGNone",
|
||||
CompressionPNGSpeed: "CompressionPNGSpeed",
|
||||
CompressionPNGBest: "CompressionPNGBest",
|
||||
CompressionJPEG100: "CompressionJPEG100",
|
||||
CompressionJPEG90: "CompressionJPEG90",
|
||||
CompressionJPEG80: "CompressionJPEG80",
|
||||
CompressionJPEG70: "CompressionJPEG70",
|
||||
CompressionJPEG60: "CompressionJPEG60",
|
||||
CompressionJPEG50: "CompressionJPEG50",
|
||||
CompressionJPEG25: "CompressionJPEG25",
|
||||
CompressionJPEG10: "CompressionJPEG10",
|
||||
CompressionJPEG1: "CompressionJPEG1",
|
||||
}
|
||||
|
||||
func (e ImageCompresson) Valid() bool {
|
||||
return langext.InArray(e, __ImageCompressonValues)
|
||||
}
|
||||
|
||||
func (e ImageCompresson) Values() []ImageCompresson {
|
||||
return __ImageCompressonValues
|
||||
}
|
||||
|
||||
func (e ImageCompresson) ValuesAny() []any {
|
||||
return langext.ArrCastToAny(__ImageCompressonValues)
|
||||
}
|
||||
|
||||
func (e ImageCompresson) ValuesMeta() []enums.EnumMetaValue {
|
||||
return ImageCompressonValuesMeta()
|
||||
}
|
||||
|
||||
func (e ImageCompresson) String() string {
|
||||
return string(e)
|
||||
}
|
||||
|
||||
func (e ImageCompresson) VarName() string {
|
||||
if d, ok := __ImageCompressonVarnames[e]; ok {
|
||||
return d
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (e ImageCompresson) TypeName() string {
|
||||
return "ImageCompresson"
|
||||
}
|
||||
|
||||
func (e ImageCompresson) PackageName() string {
|
||||
return "media"
|
||||
}
|
||||
|
||||
func (e ImageCompresson) Meta() enums.EnumMetaValue {
|
||||
return enums.EnumMetaValue{VarName: e.VarName(), Value: e, Description: nil}
|
||||
}
|
||||
|
||||
func ParseImageCompresson(vv string) (ImageCompresson, bool) {
|
||||
for _, ev := range __ImageCompressonValues {
|
||||
if string(ev) == vv {
|
||||
return ev, true
|
||||
}
|
||||
}
|
||||
return "", false
|
||||
}
|
||||
|
||||
func ImageCompressonValues() []ImageCompresson {
|
||||
return __ImageCompressonValues
|
||||
}
|
||||
|
||||
func ImageCompressonValuesMeta() []enums.EnumMetaValue {
|
||||
return []enums.EnumMetaValue{
|
||||
CompressionPNGNone.Meta(),
|
||||
CompressionPNGSpeed.Meta(),
|
||||
CompressionPNGBest.Meta(),
|
||||
CompressionJPEG100.Meta(),
|
||||
CompressionJPEG90.Meta(),
|
||||
CompressionJPEG80.Meta(),
|
||||
CompressionJPEG70.Meta(),
|
||||
CompressionJPEG60.Meta(),
|
||||
CompressionJPEG50.Meta(),
|
||||
CompressionJPEG25.Meta(),
|
||||
CompressionJPEG10.Meta(),
|
||||
CompressionJPEG1.Meta(),
|
||||
}
|
||||
}
|
||||
|
||||
// ================================ MimeCategory ================================
|
||||
//
|
||||
// File: mime.go
|
||||
// StringEnum: true
|
||||
// DescrEnum: false
|
||||
// DataEnum: false
|
||||
//
|
||||
|
||||
var __MimeCategoryValues = []MimeCategory{
|
||||
CatDocument,
|
||||
CatImage,
|
||||
CatVideo,
|
||||
CatContainer,
|
||||
}
|
||||
|
||||
var __MimeCategoryVarnames = map[MimeCategory]string{
|
||||
CatDocument: "CatDocument",
|
||||
CatImage: "CatImage",
|
||||
CatVideo: "CatVideo",
|
||||
CatContainer: "CatContainer",
|
||||
}
|
||||
|
||||
func (e MimeCategory) Valid() bool {
|
||||
return langext.InArray(e, __MimeCategoryValues)
|
||||
}
|
||||
|
||||
func (e MimeCategory) Values() []MimeCategory {
|
||||
return __MimeCategoryValues
|
||||
}
|
||||
|
||||
func (e MimeCategory) ValuesAny() []any {
|
||||
return langext.ArrCastToAny(__MimeCategoryValues)
|
||||
}
|
||||
|
||||
func (e MimeCategory) ValuesMeta() []enums.EnumMetaValue {
|
||||
return MimeCategoryValuesMeta()
|
||||
}
|
||||
|
||||
func (e MimeCategory) String() string {
|
||||
return string(e)
|
||||
}
|
||||
|
||||
func (e MimeCategory) VarName() string {
|
||||
if d, ok := __MimeCategoryVarnames[e]; ok {
|
||||
return d
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (e MimeCategory) TypeName() string {
|
||||
return "MimeCategory"
|
||||
}
|
||||
|
||||
func (e MimeCategory) PackageName() string {
|
||||
return "media"
|
||||
}
|
||||
|
||||
func (e MimeCategory) Meta() enums.EnumMetaValue {
|
||||
return enums.EnumMetaValue{VarName: e.VarName(), Value: e, Description: nil}
|
||||
}
|
||||
|
||||
func ParseMimeCategory(vv string) (MimeCategory, bool) {
|
||||
for _, ev := range __MimeCategoryValues {
|
||||
if string(ev) == vv {
|
||||
return ev, true
|
||||
}
|
||||
}
|
||||
return "", false
|
||||
}
|
||||
|
||||
func MimeCategoryValues() []MimeCategory {
|
||||
return __MimeCategoryValues
|
||||
}
|
||||
|
||||
func MimeCategoryValuesMeta() []enums.EnumMetaValue {
|
||||
return []enums.EnumMetaValue{
|
||||
CatDocument.Meta(),
|
||||
CatImage.Meta(),
|
||||
CatVideo.Meta(),
|
||||
CatContainer.Meta(),
|
||||
}
|
||||
}
|
||||
|
||||
// ================================ ================= ================================
|
||||
|
||||
func AllPackageEnums() []enums.Enum {
|
||||
return []enums.Enum{
|
||||
ImageFitStretch, // ImageFit
|
||||
CompressionPNGNone, // ImageCompresson
|
||||
CatDocument, // MimeCategory
|
||||
}
|
||||
}
|
317
imageext/image.go
Normal file
317
imageext/image.go
Normal file
@ -0,0 +1,317 @@
|
||||
package imageext
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"github.com/disintegration/imaging"
|
||||
"gogs.mikescher.com/BlackForestBytes/goext/exerr"
|
||||
"gogs.mikescher.com/BlackForestBytes/goext/mathext"
|
||||
"image"
|
||||
"image/color"
|
||||
"image/draw"
|
||||
"image/jpeg"
|
||||
"image/png"
|
||||
"io"
|
||||
"math"
|
||||
)
|
||||
|
||||
type ImageFit string //@enum:type
|
||||
|
||||
const (
|
||||
ImageFitStretch ImageFit = "STRETCH"
|
||||
ImageFitCover ImageFit = "COVER"
|
||||
ImageFitContainCenter ImageFit = "CONTAIN_CENTER"
|
||||
ImageFitContainTopLeft ImageFit = "CONTAIN_TOPLEFT"
|
||||
ImageFitContainTopRight ImageFit = "CONTAIN_TOPRIGHT"
|
||||
ImageFitContainBottomLeft ImageFit = "CONTAIN_BOTTOMLEFT"
|
||||
ImageFitContainBottomRight ImageFit = "CONTAIN_BOTTOMRIGHT"
|
||||
)
|
||||
|
||||
type ImageCrop struct { // all crop values are percentages!
|
||||
|
||||
CropX float64 `bson:"cropX" json:"cropX"`
|
||||
CropY float64 `bson:"cropY" json:"cropY"`
|
||||
CropWidth float64 `bson:"cropWidth" json:"cropWidth"`
|
||||
CropHeight float64 `bson:"cropHeight" json:"cropHeight"`
|
||||
}
|
||||
|
||||
type ImageCompresson string //@enum:type
|
||||
|
||||
const (
|
||||
CompressionPNGNone ImageCompresson = "PNG_NONE"
|
||||
CompressionPNGSpeed ImageCompresson = "PNG_SPEED"
|
||||
CompressionPNGBest ImageCompresson = "PNG_BEST"
|
||||
CompressionJPEG100 ImageCompresson = "JPEG_100"
|
||||
CompressionJPEG90 ImageCompresson = "JPEG_090"
|
||||
CompressionJPEG80 ImageCompresson = "JPEG_080"
|
||||
CompressionJPEG70 ImageCompresson = "JPEG_070"
|
||||
CompressionJPEG60 ImageCompresson = "JPEG_060"
|
||||
CompressionJPEG50 ImageCompresson = "JPEG_050"
|
||||
CompressionJPEG25 ImageCompresson = "JPEG_025"
|
||||
CompressionJPEG10 ImageCompresson = "JPEG_010"
|
||||
CompressionJPEG1 ImageCompresson = "JPEG_001"
|
||||
)
|
||||
|
||||
func CropImage(img image.Image, px float64, py float64, pw float64, ph float64) (image.Image, error) {
|
||||
|
||||
type subImager interface {
|
||||
SubImage(r image.Rectangle) image.Image
|
||||
}
|
||||
|
||||
x := int(float64(img.Bounds().Dx()) * px)
|
||||
y := int(float64(img.Bounds().Dy()) * py)
|
||||
w := int(float64(img.Bounds().Dx()) * pw)
|
||||
h := int(float64(img.Bounds().Dy()) * ph)
|
||||
|
||||
if simg, ok := img.(subImager); ok {
|
||||
|
||||
return simg.SubImage(image.Rect(x, y, x+w, y+h)), nil
|
||||
|
||||
} else {
|
||||
|
||||
bfr1 := bytes.Buffer{}
|
||||
err := png.Encode(&bfr1, img)
|
||||
if err != nil {
|
||||
return nil, exerr.Wrap(err, "").Build()
|
||||
}
|
||||
imgPNG, err := png.Decode(&bfr1)
|
||||
if err != nil {
|
||||
return nil, exerr.Wrap(err, "").Build()
|
||||
}
|
||||
|
||||
return imgPNG.(subImager).SubImage(image.Rect(x, y, w+w, y+h)), nil
|
||||
}
|
||||
}
|
||||
|
||||
func EncodeImage(img image.Image, compression ImageCompresson) (bytes.Buffer, string, error) {
|
||||
var err error
|
||||
|
||||
bfr := bytes.Buffer{}
|
||||
|
||||
switch compression {
|
||||
case CompressionPNGNone:
|
||||
enc := &png.Encoder{CompressionLevel: png.NoCompression}
|
||||
err = enc.Encode(&bfr, img)
|
||||
if err != nil {
|
||||
return bytes.Buffer{}, "", exerr.Wrap(err, "").Build()
|
||||
}
|
||||
return bfr, "image/png", nil
|
||||
case CompressionPNGSpeed:
|
||||
enc := &png.Encoder{CompressionLevel: png.BestSpeed}
|
||||
err = enc.Encode(&bfr, img)
|
||||
if err != nil {
|
||||
return bytes.Buffer{}, "", exerr.Wrap(err, "").Build()
|
||||
}
|
||||
return bfr, "image/png", nil
|
||||
case CompressionPNGBest:
|
||||
enc := &png.Encoder{CompressionLevel: png.BestCompression}
|
||||
err = enc.Encode(&bfr, img)
|
||||
if err != nil {
|
||||
return bytes.Buffer{}, "", exerr.Wrap(err, "").Build()
|
||||
}
|
||||
return bfr, "image/png", nil
|
||||
case CompressionJPEG100:
|
||||
err = jpeg.Encode(&bfr, img, &jpeg.Options{Quality: 100})
|
||||
if err != nil {
|
||||
return bytes.Buffer{}, "", exerr.Wrap(err, "").Build()
|
||||
}
|
||||
return bfr, "image/jpeg", nil
|
||||
case CompressionJPEG90:
|
||||
err = jpeg.Encode(&bfr, img, &jpeg.Options{Quality: 90})
|
||||
if err != nil {
|
||||
return bytes.Buffer{}, "", exerr.Wrap(err, "").Build()
|
||||
}
|
||||
return bfr, "image/jpeg", nil
|
||||
case CompressionJPEG80:
|
||||
err = jpeg.Encode(&bfr, img, &jpeg.Options{Quality: 80})
|
||||
if err != nil {
|
||||
return bytes.Buffer{}, "", exerr.Wrap(err, "").Build()
|
||||
}
|
||||
return bfr, "image/jpeg", nil
|
||||
case CompressionJPEG70:
|
||||
err = jpeg.Encode(&bfr, img, &jpeg.Options{Quality: 70})
|
||||
if err != nil {
|
||||
return bytes.Buffer{}, "", exerr.Wrap(err, "").Build()
|
||||
}
|
||||
return bfr, "image/jpeg", nil
|
||||
case CompressionJPEG60:
|
||||
err = jpeg.Encode(&bfr, img, &jpeg.Options{Quality: 60})
|
||||
if err != nil {
|
||||
return bytes.Buffer{}, "", exerr.Wrap(err, "").Build()
|
||||
}
|
||||
return bfr, "image/jpeg", nil
|
||||
case CompressionJPEG50:
|
||||
err = jpeg.Encode(&bfr, img, &jpeg.Options{Quality: 50})
|
||||
if err != nil {
|
||||
return bytes.Buffer{}, "", exerr.Wrap(err, "").Build()
|
||||
}
|
||||
return bfr, "image/jpeg", nil
|
||||
case CompressionJPEG25:
|
||||
err = jpeg.Encode(&bfr, img, &jpeg.Options{Quality: 25})
|
||||
if err != nil {
|
||||
return bytes.Buffer{}, "", exerr.Wrap(err, "").Build()
|
||||
}
|
||||
return bfr, "image/jpeg", nil
|
||||
case CompressionJPEG10:
|
||||
err = jpeg.Encode(&bfr, img, &jpeg.Options{Quality: 10})
|
||||
if err != nil {
|
||||
return bytes.Buffer{}, "", exerr.Wrap(err, "").Build()
|
||||
}
|
||||
return bfr, "image/jpeg", nil
|
||||
case CompressionJPEG1:
|
||||
err = jpeg.Encode(&bfr, img, &jpeg.Options{Quality: 1})
|
||||
if err != nil {
|
||||
return bytes.Buffer{}, "", exerr.Wrap(err, "").Build()
|
||||
}
|
||||
return bfr, "image/jpeg", nil
|
||||
default:
|
||||
return bytes.Buffer{}, "", exerr.New(exerr.TypeInternal, "unknown compression method: "+compression.String()).Build()
|
||||
}
|
||||
}
|
||||
|
||||
func ObjectFitImage(img image.Image, bbw float64, bbh float64, fit ImageFit, fillColor color.Color) (image.Image, error) {
|
||||
|
||||
iw := img.Bounds().Size().X
|
||||
ih := img.Bounds().Size().Y
|
||||
|
||||
// [iw, ih] is the size of the image
|
||||
// [bbw, bbh] is the target bounding box,
|
||||
// - it specifies the target ratio
|
||||
// - and the maximal target resolution
|
||||
|
||||
facW := float64(iw) / bbw
|
||||
facH := float64(ih) / bbh
|
||||
|
||||
// facW is the ratio between iw and bbw
|
||||
// - it is the factor by which the bounding box must be multiplied to reach the image size (in the x-axis)
|
||||
//
|
||||
// (same is true for facH, but for the height and y-axis)
|
||||
|
||||
if fit == ImageFitCover {
|
||||
|
||||
// image-fit:cover completely fills the target-bounding-box, it potentially cuts parts of the image away
|
||||
|
||||
// we use the smaller (!) value of facW and facH, because we want to have the smallest possible destination rect (due to file size)
|
||||
// and because the image is made to completely fill the bounding-box, the smaller factor (= teh dimension the image is stretched more) is relevant
|
||||
|
||||
// but we cap `fac` at 1 (can be larger than 1)
|
||||
// a value >1 would mean the final image resolution is biger than the bounding box, which we do not want.
|
||||
|
||||
// if the initial image (iw, ih) is already bigger than the bounding box (bbw, bbh), facW and facH are always >1 and fac will be 1
|
||||
// which means we will simply use the bounding box as destination rect (and scale the image down)
|
||||
|
||||
fac := mathext.Clamp(mathext.Min(facW, facH), 0.0, 1.0)
|
||||
|
||||
// we scale the bounding box by fac (both dimension the same amount, to keep the bounding-box ratio)
|
||||
|
||||
w := int(math.Round(bbw * fac))
|
||||
h := int(math.Round(bbh * fac))
|
||||
|
||||
img = imaging.Fill(img, w, h, imaging.Center, imaging.Lanczos)
|
||||
|
||||
newImg := image.NewRGBA(image.Rect(0, 0, w, h))
|
||||
|
||||
draw.Draw(newImg, newImg.Bounds(), &image.Uniform{C: fillColor}, image.Pt(0, 0), draw.Src)
|
||||
draw.Draw(newImg, newImg.Bounds(), img, image.Pt(0, 0), draw.Over)
|
||||
|
||||
return newImg, nil
|
||||
}
|
||||
|
||||
if fit == ImageFitContainCenter || fit == ImageFitContainTopLeft || fit == ImageFitContainTopRight || fit == ImageFitContainBottomLeft || fit == ImageFitContainBottomRight {
|
||||
|
||||
// image-fit:cover fills the target-bounding-box with the image, there is potentially empty-space, it potentially cuts parts of the image away
|
||||
|
||||
// we use the bigger (!) value of facW and facH,
|
||||
// because the image is made to fit the bounding-box, the bigger factor (= the dimension the image is stretched less) is relevant
|
||||
|
||||
// but we cap `fac` at 1 (can be larger than 1)
|
||||
// a value >1 would mean the final image resolution is biger than the bounding box, which we do not want.
|
||||
|
||||
// if the initial image (iw, ih) is already bigger than the bounding box (bbw, bbh), facW and facH are always >1 and fac will be 1
|
||||
// which means we will simply use the bounding box as destination rect (and scale the image down)
|
||||
|
||||
facOut := mathext.Clamp(mathext.Max(facW, facH), 0.0, 1.0)
|
||||
|
||||
// we scale the bounding box by fac (both dimension the same amount, to keep the bounding-box ratio)
|
||||
|
||||
ow := int(math.Round(bbw * facOut))
|
||||
oh := int(math.Round(bbh * facOut))
|
||||
|
||||
facScale := mathext.Min(float64(ow)/float64(iw), float64(oh)/float64(ih))
|
||||
|
||||
dw := int(math.Round(float64(iw) * facScale))
|
||||
dh := int(math.Round(float64(ih) * facScale))
|
||||
|
||||
img = imaging.Resize(img, dw, dh, imaging.Lanczos)
|
||||
|
||||
var destBounds image.Rectangle
|
||||
if fit == ImageFitContainCenter {
|
||||
destBounds = image.Rect((ow-dw)/2, (oh-dh)/2, (ow-dw)/2+dw, (oh-dh)/2+dh)
|
||||
} else if fit == ImageFitContainTopLeft {
|
||||
destBounds = image.Rect(0, 0, iw, dh)
|
||||
} else if fit == ImageFitContainTopRight {
|
||||
destBounds = image.Rect(ow-iw, 0, ow, dh)
|
||||
} else if fit == ImageFitContainBottomLeft {
|
||||
destBounds = image.Rect(0, oh-dh, iw, oh)
|
||||
} else if fit == ImageFitContainBottomRight {
|
||||
destBounds = image.Rect(ow-dw, oh-dh, ow, oh)
|
||||
}
|
||||
|
||||
newImg := image.NewRGBA(image.Rect(0, 0, ow, oh))
|
||||
|
||||
draw.Draw(newImg, newImg.Bounds(), &image.Uniform{C: fillColor}, image.Pt(0, 0), draw.Src)
|
||||
draw.Draw(newImg, destBounds, img, image.Pt(0, 0), draw.Over)
|
||||
|
||||
return newImg, nil
|
||||
}
|
||||
|
||||
if fit == ImageFitStretch {
|
||||
|
||||
// image-fit:stretch simply stretches the image to the bounding box
|
||||
|
||||
// we use the bigger value of [facW;facH], to (potentially) scale the bounding box down before applying it
|
||||
// theoretically we could directly use [bbw, bbh] in the call to imaging.Resize,
|
||||
// but if the image is (a lot) smaller than the bouding box it is useful to scale it down to reduce final pdf filesize
|
||||
|
||||
// we also cap fac at 1, because we never want the final rect to be bigger than the inputted bounding box (see comments at start of method)
|
||||
|
||||
fac := mathext.Clamp(mathext.Max(facW, facH), 0.0, 1.0)
|
||||
|
||||
// we scale the bounding box by fac (both dimension the same amount, to keep the bounding-box ratio)
|
||||
|
||||
w := int(math.Round(bbw * fac))
|
||||
h := int(math.Round(bbh * fac))
|
||||
|
||||
img = imaging.Resize(img, w, h, imaging.Lanczos)
|
||||
|
||||
newImg := image.NewRGBA(image.Rect(0, 0, w, h))
|
||||
|
||||
draw.Draw(newImg, newImg.Bounds(), &image.Uniform{C: fillColor}, image.Pt(0, 0), draw.Src)
|
||||
draw.Draw(newImg, newImg.Bounds(), img, image.Pt(0, 0), draw.Over)
|
||||
|
||||
return newImg, nil
|
||||
}
|
||||
|
||||
return nil, exerr.New(exerr.TypeInternal, fmt.Sprintf("unknown image-fit: '%s'", fit)).Build()
|
||||
}
|
||||
|
||||
func VerifyAndDecodeImage(data io.Reader, mime string) (image.Image, error) {
|
||||
|
||||
if mime == "image/jpeg" {
|
||||
img, err := jpeg.Decode(data)
|
||||
if err != nil {
|
||||
return nil, exerr.Wrap(err, "failed to decode blob as jpeg").WithType(exerr.TypeInvalidImage).Build()
|
||||
}
|
||||
return img, nil
|
||||
}
|
||||
|
||||
if mime == "image/png" {
|
||||
img, err := png.Decode(data)
|
||||
if err != nil {
|
||||
return nil, exerr.Wrap(err, "failed to decode blob as png").WithType(exerr.TypeInvalidImage).Build()
|
||||
}
|
||||
return img, nil
|
||||
}
|
||||
|
||||
return nil, exerr.New(exerr.TypeInvalidMimeType, fmt.Sprintf("unknown/invalid image mimetype: '%s'", mime)).Build()
|
||||
}
|
@ -3,13 +3,19 @@ package wpdf
|
||||
import (
|
||||
"bytes"
|
||||
"github.com/jung-kurt/gofpdf"
|
||||
"gogs.mikescher.com/BlackForestBytes/goext/imageext"
|
||||
"gogs.mikescher.com/BlackForestBytes/goext/langext"
|
||||
"image"
|
||||
"image/color"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
type PDFImageRef struct {
|
||||
Info *gofpdf.ImageInfoType
|
||||
Name string
|
||||
Bin []byte
|
||||
Image *image.Image
|
||||
Mime string
|
||||
}
|
||||
|
||||
type PDFImageRegisterOpt struct {
|
||||
@ -48,6 +54,7 @@ func (b *WPDFBuilder) RegisterImage(bin []byte, opts ...*PDFImageRegisterOpt) *P
|
||||
imageType := ""
|
||||
readDpi := false
|
||||
allowNegativePosition := false
|
||||
mime := "application/octet-stream"
|
||||
|
||||
for _, opt := range opts {
|
||||
imageType = langext.Coalesce(opt.imageType, imageType)
|
||||
@ -61,12 +68,26 @@ func (b *WPDFBuilder) RegisterImage(bin []byte, opts ...*PDFImageRegisterOpt) *P
|
||||
switch ct {
|
||||
case "image/jpg":
|
||||
imageType = "JPG"
|
||||
mime = ct
|
||||
case "image/jpeg":
|
||||
imageType = "JPEG"
|
||||
mime = ct
|
||||
case "image/png":
|
||||
imageType = "PNG"
|
||||
mime = ct
|
||||
case "image/gif":
|
||||
imageType = "GIF"
|
||||
mime = ct
|
||||
}
|
||||
} else {
|
||||
switch imageType {
|
||||
case "JPG":
|
||||
case "JPEG":
|
||||
mime = "image/jpeg"
|
||||
case "PNG":
|
||||
mime = "image/png"
|
||||
case "GIF":
|
||||
mime = "image/gif"
|
||||
}
|
||||
}
|
||||
|
||||
@ -81,6 +102,9 @@ func (b *WPDFBuilder) RegisterImage(bin []byte, opts ...*PDFImageRegisterOpt) *P
|
||||
return &PDFImageRef{
|
||||
Name: imgName,
|
||||
Info: info,
|
||||
Bin: bin,
|
||||
Image: nil,
|
||||
Mime: mime,
|
||||
}
|
||||
}
|
||||
|
||||
@ -95,6 +119,11 @@ type PDFImageOpt struct {
|
||||
imageType *string
|
||||
readDpi *bool
|
||||
allowNegativePosition *bool
|
||||
imageFit *imageext.ImageFit
|
||||
fillColor *color.Color
|
||||
compression *imageext.ImageCompresson
|
||||
reEncodePixelPerMM *float64
|
||||
crop *imageext.ImageCrop
|
||||
}
|
||||
|
||||
func NewPDFImageOpt() *PDFImageOpt {
|
||||
@ -151,7 +180,40 @@ func (opt *PDFImageOpt) AllowNegativePosition(v bool) *PDFImageOpt {
|
||||
return opt
|
||||
}
|
||||
|
||||
func (opt *PDFImageOpt) ImageFit(v imageext.ImageFit) *PDFImageOpt {
|
||||
opt.imageFit = &v
|
||||
return opt
|
||||
|
||||
}
|
||||
|
||||
func (opt *PDFImageOpt) FillColor(v color.Color) *PDFImageOpt {
|
||||
opt.fillColor = &v
|
||||
return opt
|
||||
}
|
||||
|
||||
func (opt *PDFImageOpt) Compression(v imageext.ImageCompresson) *PDFImageOpt {
|
||||
opt.compression = &v
|
||||
return opt
|
||||
}
|
||||
|
||||
func (opt *PDFImageOpt) ReEncodePixelPerMM(v float64) *PDFImageOpt {
|
||||
opt.reEncodePixelPerMM = &v
|
||||
return opt
|
||||
}
|
||||
|
||||
func (opt *PDFImageOpt) Crop(cropX float64, cropY float64, cropWidth float64, cropHeight float64) *PDFImageOpt {
|
||||
opt.crop = &imageext.ImageCrop{
|
||||
CropX: cropX,
|
||||
CropY: cropY,
|
||||
CropWidth: cropWidth,
|
||||
CropHeight: cropHeight,
|
||||
}
|
||||
return opt
|
||||
}
|
||||
|
||||
func (b *WPDFBuilder) Image(img *PDFImageRef, opts ...*PDFImageOpt) {
|
||||
var err error
|
||||
|
||||
x := b.GetX()
|
||||
y := b.GetY()
|
||||
w := img.Info.Width()
|
||||
@ -162,6 +224,11 @@ func (b *WPDFBuilder) Image(img *PDFImageRef, opts ...*PDFImageOpt) {
|
||||
imageType := ""
|
||||
readDpi := false
|
||||
allowNegativePosition := false
|
||||
reEncodePixelPerMM := 15.0
|
||||
var imageFit *imageext.ImageFit = nil
|
||||
var fillColor color.Color = color.Transparent
|
||||
compression := imageext.CompressionPNGSpeed
|
||||
var crop *imageext.ImageCrop = nil
|
||||
|
||||
for _, opt := range opts {
|
||||
x = langext.Coalesce(opt.x, x)
|
||||
@ -174,6 +241,68 @@ func (b *WPDFBuilder) Image(img *PDFImageRef, opts ...*PDFImageOpt) {
|
||||
imageType = langext.Coalesce(opt.imageType, imageType)
|
||||
readDpi = langext.Coalesce(opt.readDpi, readDpi)
|
||||
allowNegativePosition = langext.Coalesce(opt.allowNegativePosition, allowNegativePosition)
|
||||
imageFit = langext.CoalesceOpt(opt.imageFit, imageFit)
|
||||
fillColor = langext.Coalesce(opt.fillColor, fillColor)
|
||||
compression = langext.Coalesce(opt.compression, compression)
|
||||
reEncodePixelPerMM = langext.Coalesce(opt.reEncodePixelPerMM, reEncodePixelPerMM)
|
||||
crop = langext.CoalesceOpt(opt.crop, crop)
|
||||
}
|
||||
|
||||
regName := img.Name
|
||||
|
||||
if imageFit != nil || fillColor != nil || crop != nil {
|
||||
|
||||
var dataimg image.Image
|
||||
if img.Image != nil {
|
||||
dataimg = *img.Image
|
||||
} else {
|
||||
dataimg, err = imageext.VerifyAndDecodeImage(bytes.NewReader(img.Bin), img.Mime)
|
||||
if err != nil {
|
||||
b.b.SetError(err)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if crop != nil {
|
||||
dataimg, err = imageext.CropImage(dataimg, crop.CropX, crop.CropY, crop.CropWidth, crop.CropHeight)
|
||||
if err != nil {
|
||||
b.b.SetError(err)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if imageFit != nil {
|
||||
pdfPixelPerMillimeter := 15.0
|
||||
|
||||
pxw := w * pdfPixelPerMillimeter
|
||||
pxh := h * pdfPixelPerMillimeter
|
||||
|
||||
dataimg, err = imageext.ObjectFitImage(dataimg, pxw, pxh, *imageFit, fillColor)
|
||||
if err != nil {
|
||||
b.b.SetError(err)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
bfr, imgMime, err := imageext.EncodeImage(dataimg, compression)
|
||||
if err != nil {
|
||||
b.b.SetError(err)
|
||||
return
|
||||
}
|
||||
|
||||
regName = regName + "_" + langext.MustRawHexUUID()
|
||||
|
||||
switch imgMime {
|
||||
case "image/jpeg":
|
||||
imageType = "JPEG"
|
||||
case "image/png":
|
||||
imageType = "PNG"
|
||||
case "image/gif":
|
||||
imageType = "GIF"
|
||||
}
|
||||
|
||||
b.b.RegisterImageOptionsReader(regName, gofpdf.ImageOptions{ImageType: imageType}, &bfr)
|
||||
|
||||
}
|
||||
|
||||
fpdfOpt := gofpdf.ImageOptions{
|
||||
@ -182,5 +311,5 @@ func (b *WPDFBuilder) Image(img *PDFImageRef, opts ...*PDFImageOpt) {
|
||||
AllowNegativePosition: allowNegativePosition,
|
||||
}
|
||||
|
||||
b.b.ImageOptions(img.Name, x, y, w, h, flow, fpdfOpt, link, linkStr)
|
||||
b.b.ImageOptions(regName, x, y, w, h, flow, fpdfOpt, link, linkStr)
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user