513 lines
14 KiB
Go
513 lines
14 KiB
Go
package vips
|
|
|
|
// #include "foreign.h"
|
|
import "C"
|
|
import (
|
|
"bytes"
|
|
"encoding/xml"
|
|
"fmt"
|
|
"golang.org/x/image/bmp"
|
|
"golang.org/x/net/html/charset"
|
|
"image/png"
|
|
"math"
|
|
"runtime"
|
|
"unsafe"
|
|
)
|
|
|
|
// SubsampleMode correlates to a libvips subsample mode
|
|
type SubsampleMode int
|
|
|
|
// SubsampleMode enum correlating to libvips subsample modes
|
|
const (
|
|
VipsForeignSubsampleAuto SubsampleMode = C.VIPS_FOREIGN_SUBSAMPLE_AUTO
|
|
VipsForeignSubsampleOn SubsampleMode = C.VIPS_FOREIGN_SUBSAMPLE_ON
|
|
VipsForeignSubsampleOff SubsampleMode = C.VIPS_FOREIGN_SUBSAMPLE_OFF
|
|
VipsForeignSubsampleLast SubsampleMode = C.VIPS_FOREIGN_SUBSAMPLE_LAST
|
|
)
|
|
|
|
// ImageType represents an image type
|
|
type ImageType int
|
|
|
|
// ImageType enum
|
|
const (
|
|
ImageTypeUnknown ImageType = C.UNKNOWN
|
|
ImageTypeGIF ImageType = C.GIF
|
|
ImageTypeJPEG ImageType = C.JPEG
|
|
ImageTypeMagick ImageType = C.MAGICK
|
|
ImageTypePDF ImageType = C.PDF
|
|
ImageTypePNG ImageType = C.PNG
|
|
ImageTypeSVG ImageType = C.SVG
|
|
ImageTypeTIFF ImageType = C.TIFF
|
|
ImageTypeWEBP ImageType = C.WEBP
|
|
ImageTypeHEIF ImageType = C.HEIF
|
|
ImageTypeBMP ImageType = C.BMP
|
|
ImageTypeAVIF ImageType = C.AVIF
|
|
ImageTypeJP2K ImageType = C.JP2K
|
|
ImageTypeJXL ImageType = C.JXL
|
|
)
|
|
|
|
var imageTypeExtensionMap = map[ImageType]string{
|
|
ImageTypeGIF: ".gif",
|
|
ImageTypeJPEG: ".jpeg",
|
|
ImageTypeMagick: ".magick",
|
|
ImageTypePDF: ".pdf",
|
|
ImageTypePNG: ".png",
|
|
ImageTypeSVG: ".svg",
|
|
ImageTypeTIFF: ".tiff",
|
|
ImageTypeWEBP: ".webp",
|
|
ImageTypeHEIF: ".heic",
|
|
ImageTypeBMP: ".bmp",
|
|
ImageTypeAVIF: ".avif",
|
|
ImageTypeJP2K: ".jp2",
|
|
ImageTypeJXL: ".jxl",
|
|
}
|
|
|
|
// ImageTypes defines the various image types supported by govips
|
|
var ImageTypes = map[ImageType]string{
|
|
ImageTypeGIF: "gif",
|
|
ImageTypeJPEG: "jpeg",
|
|
ImageTypeMagick: "magick",
|
|
ImageTypePDF: "pdf",
|
|
ImageTypePNG: "png",
|
|
ImageTypeSVG: "svg",
|
|
ImageTypeTIFF: "tiff",
|
|
ImageTypeWEBP: "webp",
|
|
ImageTypeHEIF: "heif",
|
|
ImageTypeBMP: "bmp",
|
|
ImageTypeAVIF: "heif",
|
|
ImageTypeJP2K: "jp2k",
|
|
ImageTypeJXL: "jxl",
|
|
}
|
|
|
|
// TiffCompression represents method for compressing a tiff at export
|
|
type TiffCompression int
|
|
|
|
// TiffCompression enum
|
|
const (
|
|
TiffCompressionNone TiffCompression = C.VIPS_FOREIGN_TIFF_COMPRESSION_NONE
|
|
TiffCompressionJpeg TiffCompression = C.VIPS_FOREIGN_TIFF_COMPRESSION_JPEG
|
|
TiffCompressionDeflate TiffCompression = C.VIPS_FOREIGN_TIFF_COMPRESSION_DEFLATE
|
|
TiffCompressionPackbits TiffCompression = C.VIPS_FOREIGN_TIFF_COMPRESSION_PACKBITS
|
|
TiffCompressionFax4 TiffCompression = C.VIPS_FOREIGN_TIFF_COMPRESSION_CCITTFAX4
|
|
TiffCompressionLzw TiffCompression = C.VIPS_FOREIGN_TIFF_COMPRESSION_LZW
|
|
TiffCompressionWebp TiffCompression = C.VIPS_FOREIGN_TIFF_COMPRESSION_WEBP
|
|
TiffCompressionZstd TiffCompression = C.VIPS_FOREIGN_TIFF_COMPRESSION_ZSTD
|
|
)
|
|
|
|
// TiffPredictor represents method for compressing a tiff at export
|
|
type TiffPredictor int
|
|
|
|
// TiffPredictor enum
|
|
const (
|
|
TiffPredictorNone TiffPredictor = C.VIPS_FOREIGN_TIFF_PREDICTOR_NONE
|
|
TiffPredictorHorizontal TiffPredictor = C.VIPS_FOREIGN_TIFF_PREDICTOR_HORIZONTAL
|
|
TiffPredictorFloat TiffPredictor = C.VIPS_FOREIGN_TIFF_PREDICTOR_FLOAT
|
|
)
|
|
|
|
// PngFilter represents filter algorithms that can be applied before compression.
|
|
// See https://www.w3.org/TR/PNG-Filters.html
|
|
type PngFilter int
|
|
|
|
// PngFilter enum
|
|
const (
|
|
PngFilterNone PngFilter = C.VIPS_FOREIGN_PNG_FILTER_NONE
|
|
PngFilterSub PngFilter = C.VIPS_FOREIGN_PNG_FILTER_SUB
|
|
PngFilterUo PngFilter = C.VIPS_FOREIGN_PNG_FILTER_UP
|
|
PngFilterAvg PngFilter = C.VIPS_FOREIGN_PNG_FILTER_AVG
|
|
PngFilterPaeth PngFilter = C.VIPS_FOREIGN_PNG_FILTER_PAETH
|
|
PngFilterAll PngFilter = C.VIPS_FOREIGN_PNG_FILTER_ALL
|
|
)
|
|
|
|
// FileExt returns the canonical extension for the ImageType
|
|
func (i ImageType) FileExt() string {
|
|
if ext, ok := imageTypeExtensionMap[i]; ok {
|
|
return ext
|
|
}
|
|
return ""
|
|
}
|
|
|
|
// IsTypeSupported checks whether given image type is supported by govips
|
|
func IsTypeSupported(imageType ImageType) bool {
|
|
startupIfNeeded()
|
|
|
|
return supportedImageTypes[imageType]
|
|
}
|
|
|
|
// DetermineImageType attempts to determine the image type of the given buffer
|
|
func DetermineImageType(buf []byte) ImageType {
|
|
if len(buf) < 12 {
|
|
return ImageTypeUnknown
|
|
} else if isJPEG(buf) {
|
|
return ImageTypeJPEG
|
|
} else if isPNG(buf) {
|
|
return ImageTypePNG
|
|
} else if isGIF(buf) {
|
|
return ImageTypeGIF
|
|
} else if isTIFF(buf) {
|
|
return ImageTypeTIFF
|
|
} else if isWEBP(buf) {
|
|
return ImageTypeWEBP
|
|
} else if isAVIF(buf) {
|
|
return ImageTypeAVIF
|
|
} else if isHEIF(buf) {
|
|
return ImageTypeHEIF
|
|
} else if isSVG(buf) {
|
|
return ImageTypeSVG
|
|
} else if isBMP(buf) {
|
|
return ImageTypeBMP
|
|
} else if isJP2K(buf) {
|
|
return ImageTypeJP2K
|
|
} else if isJXL(buf) {
|
|
return ImageTypeJXL
|
|
} else if isPDF(buf) {
|
|
return ImageTypePDF
|
|
} else {
|
|
return ImageTypeUnknown
|
|
}
|
|
}
|
|
|
|
var jpeg = []byte("\xFF\xD8\xFF")
|
|
|
|
func isJPEG(buf []byte) bool {
|
|
return bytes.HasPrefix(buf, jpeg)
|
|
}
|
|
|
|
var gifHeader = []byte("\x47\x49\x46")
|
|
|
|
func isGIF(buf []byte) bool {
|
|
return bytes.HasPrefix(buf, gifHeader)
|
|
}
|
|
|
|
var pngHeader = []byte("\x89\x50\x4E\x47")
|
|
|
|
func isPNG(buf []byte) bool {
|
|
return bytes.HasPrefix(buf, pngHeader)
|
|
}
|
|
|
|
var tifII = []byte("\x49\x49\x2A\x00")
|
|
var tifMM = []byte("\x4D\x4D\x00\x2A")
|
|
|
|
func isTIFF(buf []byte) bool {
|
|
return bytes.HasPrefix(buf, tifII) || bytes.HasPrefix(buf, tifMM)
|
|
}
|
|
|
|
var webpHeader = []byte("\x57\x45\x42\x50")
|
|
|
|
func isWEBP(buf []byte) bool {
|
|
return bytes.Equal(buf[8:12], webpHeader)
|
|
}
|
|
|
|
// https://github.com/strukturag/libheif/blob/master/libheif/heif.cc
|
|
var ftyp = []byte("ftyp")
|
|
var heic = []byte("heic")
|
|
var mif1 = []byte("mif1")
|
|
var msf1 = []byte("msf1")
|
|
var avif = []byte("avif")
|
|
|
|
func isHEIF(buf []byte) bool {
|
|
return bytes.Equal(buf[4:8], ftyp) && (bytes.Equal(buf[8:12], heic) ||
|
|
bytes.Equal(buf[8:12], mif1) ||
|
|
bytes.Equal(buf[8:12], msf1)) ||
|
|
isAVIF(buf)
|
|
}
|
|
|
|
func isAVIF(buf []byte) bool {
|
|
return bytes.Equal(buf[4:8], ftyp) && bytes.Equal(buf[8:12], avif)
|
|
}
|
|
|
|
var svg = []byte("<svg")
|
|
|
|
func isSVG(buf []byte) bool {
|
|
sub := buf[:int(math.Min(1024.0, float64(len(buf))))]
|
|
if bytes.Contains(sub, svg) {
|
|
data := &struct {
|
|
XMLName xml.Name `xml:"svg"`
|
|
}{}
|
|
reader := bytes.NewReader(buf)
|
|
decoder := xml.NewDecoder(reader)
|
|
decoder.Strict = false
|
|
decoder.CharsetReader = charset.NewReaderLabel
|
|
|
|
err := decoder.Decode(data)
|
|
|
|
return err == nil && data.XMLName.Local == "svg"
|
|
}
|
|
|
|
return false
|
|
}
|
|
|
|
var pdf = []byte("\x25\x50\x44\x46")
|
|
|
|
func isPDF(buf []byte) bool {
|
|
if len(buf) <= 1024 {
|
|
return bytes.Contains(buf, pdf)
|
|
}
|
|
return bytes.Contains(buf[:1024], pdf)
|
|
}
|
|
|
|
var bmpHeader = []byte("BM")
|
|
|
|
func isBMP(buf []byte) bool {
|
|
return bytes.HasPrefix(buf, bmpHeader)
|
|
}
|
|
|
|
// X'0000 000C 6A50 2020 0D0A 870A'
|
|
var jp2kHeader = []byte("\x00\x00\x00\x0C\x6A\x50\x20\x20\x0D\x0A\x87\x0A")
|
|
|
|
// https://datatracker.ietf.org/doc/html/rfc3745
|
|
func isJP2K(buf []byte) bool {
|
|
return bytes.HasPrefix(buf, jp2kHeader)
|
|
}
|
|
|
|
// As a 'naked' codestream
|
|
var jxlHeader = []byte("\xff\x0a")
|
|
|
|
// As an ISOBMFF-based container: 0x0000000C 4A584C20 0D0A870A
|
|
var jxlHeaderISOBMFF = []byte("\x00\x00\x00\x0C\x4A\x58\x4C\x20\x0D\x0A\x87\x0A")
|
|
|
|
func isJXL(buf []byte) bool {
|
|
return bytes.HasPrefix(buf, jxlHeader) || bytes.HasPrefix(buf, jxlHeaderISOBMFF)
|
|
}
|
|
|
|
func vipsLoadFromBuffer(buf []byte, params *ImportParams) (*C.VipsImage, ImageType, ImageType, error) {
|
|
src := buf
|
|
// Reference src here so it's not garbage collected during image initialization.
|
|
defer runtime.KeepAlive(src)
|
|
|
|
var err error
|
|
|
|
originalType := DetermineImageType(src)
|
|
currentType := originalType
|
|
|
|
if originalType == ImageTypeBMP {
|
|
src, err = bmpToPNG(src)
|
|
if err != nil {
|
|
return nil, currentType, originalType, err
|
|
}
|
|
|
|
currentType = ImageTypePNG
|
|
}
|
|
|
|
if !IsTypeSupported(currentType) {
|
|
govipsLog("govips", LogLevelInfo, fmt.Sprintf("failed to understand image format size=%d", len(src)))
|
|
return nil, currentType, originalType, ErrUnsupportedImageFormat
|
|
}
|
|
|
|
importParams := createImportParams(currentType, params)
|
|
|
|
if err := C.load_from_buffer(&importParams, unsafe.Pointer(&src[0]), C.size_t(len(src))); err != 0 {
|
|
return nil, currentType, originalType, handleImageError(importParams.outputImage)
|
|
}
|
|
|
|
return importParams.outputImage, currentType, originalType, nil
|
|
}
|
|
|
|
func bmpToPNG(src []byte) ([]byte, error) {
|
|
i, err := bmp.Decode(bytes.NewReader(src))
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
var w bytes.Buffer
|
|
pngEnc := png.Encoder{
|
|
CompressionLevel: png.NoCompression,
|
|
}
|
|
err = pngEnc.Encode(&w, i)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return w.Bytes(), nil
|
|
}
|
|
|
|
func maybeSetBoolParam(p BoolParameter, cp *C.Param) {
|
|
if p.IsSet() {
|
|
C.set_bool_param(cp, toGboolean(p.Get()))
|
|
}
|
|
}
|
|
|
|
func maybeSetIntParam(p IntParameter, cp *C.Param) {
|
|
if p.IsSet() {
|
|
C.set_int_param(cp, C.int(p.Get()))
|
|
}
|
|
}
|
|
|
|
func createImportParams(format ImageType, params *ImportParams) C.LoadParams {
|
|
p := C.create_load_params(C.ImageType(format))
|
|
|
|
maybeSetBoolParam(params.AutoRotate, &p.autorotate)
|
|
maybeSetBoolParam(params.FailOnError, &p.fail)
|
|
maybeSetIntParam(params.Page, &p.page)
|
|
maybeSetIntParam(params.NumPages, &p.n)
|
|
maybeSetIntParam(params.JpegShrinkFactor, &p.jpegShrink)
|
|
maybeSetBoolParam(params.HeifThumbnail, &p.heifThumbnail)
|
|
maybeSetBoolParam(params.SvgUnlimited, &p.svgUnlimited)
|
|
|
|
if params.Density.IsSet() {
|
|
C.set_double_param(&p.dpi, C.gdouble(params.Density.Get()))
|
|
}
|
|
return p
|
|
}
|
|
|
|
func vipsSaveJPEGToBuffer(in *C.VipsImage, params JpegExportParams) ([]byte, error) {
|
|
incOpCounter("save_jpeg_buffer")
|
|
|
|
p := C.create_save_params(C.JPEG)
|
|
p.inputImage = in
|
|
p.stripMetadata = C.int(boolToInt(params.StripMetadata))
|
|
p.quality = C.int(params.Quality)
|
|
p.interlace = C.int(boolToInt(params.Interlace))
|
|
p.jpegOptimizeCoding = C.int(boolToInt(params.OptimizeCoding))
|
|
p.jpegSubsample = C.VipsForeignSubsample(params.SubsampleMode)
|
|
p.jpegTrellisQuant = C.int(boolToInt(params.TrellisQuant))
|
|
p.jpegOvershootDeringing = C.int(boolToInt(params.OvershootDeringing))
|
|
p.jpegOptimizeScans = C.int(boolToInt(params.OptimizeScans))
|
|
p.jpegQuantTable = C.int(params.QuantTable)
|
|
|
|
return vipsSaveToBuffer(p)
|
|
}
|
|
|
|
func vipsSavePNGToBuffer(in *C.VipsImage, params PngExportParams) ([]byte, error) {
|
|
incOpCounter("save_png_buffer")
|
|
|
|
p := C.create_save_params(C.PNG)
|
|
p.inputImage = in
|
|
p.quality = C.int(params.Quality)
|
|
p.stripMetadata = C.int(boolToInt(params.StripMetadata))
|
|
p.interlace = C.int(boolToInt(params.Interlace))
|
|
p.pngCompression = C.int(params.Compression)
|
|
p.pngFilter = C.VipsForeignPngFilter(params.Filter)
|
|
p.pngPalette = C.int(boolToInt(params.Palette))
|
|
p.pngDither = C.double(params.Dither)
|
|
p.pngBitdepth = C.int(params.Bitdepth)
|
|
|
|
return vipsSaveToBuffer(p)
|
|
}
|
|
|
|
func vipsSaveWebPToBuffer(in *C.VipsImage, params WebpExportParams) ([]byte, error) {
|
|
incOpCounter("save_webp_buffer")
|
|
|
|
p := C.create_save_params(C.WEBP)
|
|
p.inputImage = in
|
|
p.stripMetadata = C.int(boolToInt(params.StripMetadata))
|
|
p.quality = C.int(params.Quality)
|
|
p.webpLossless = C.int(boolToInt(params.Lossless))
|
|
p.webpNearLossless = C.int(boolToInt(params.NearLossless))
|
|
p.webpReductionEffort = C.int(params.ReductionEffort)
|
|
p.webpMinSize = C.int(boolToInt(params.MinSize))
|
|
p.webpKMin = C.int(params.MinKeyFrames)
|
|
p.webpKMax = C.int(params.MaxKeyFrames)
|
|
|
|
if params.IccProfile != "" {
|
|
p.webpIccProfile = C.CString(params.IccProfile)
|
|
defer C.free(unsafe.Pointer(p.webpIccProfile))
|
|
}
|
|
|
|
return vipsSaveToBuffer(p)
|
|
}
|
|
|
|
func vipsSaveTIFFToBuffer(in *C.VipsImage, params TiffExportParams) ([]byte, error) {
|
|
incOpCounter("save_tiff_buffer")
|
|
|
|
p := C.create_save_params(C.TIFF)
|
|
p.inputImage = in
|
|
p.stripMetadata = C.int(boolToInt(params.StripMetadata))
|
|
p.quality = C.int(params.Quality)
|
|
p.tiffCompression = C.VipsForeignTiffCompression(params.Compression)
|
|
p.tiffPyramid = C.int(boolToInt(params.Pyramid))
|
|
p.tiffTile = C.int(boolToInt(params.Tile))
|
|
p.tiffTileHeight = C.int(params.TileHeight)
|
|
p.tiffTileWidth = C.int(params.TileWidth)
|
|
|
|
return vipsSaveToBuffer(p)
|
|
}
|
|
|
|
func vipsSaveHEIFToBuffer(in *C.VipsImage, params HeifExportParams) ([]byte, error) {
|
|
incOpCounter("save_heif_buffer")
|
|
|
|
p := C.create_save_params(C.HEIF)
|
|
p.inputImage = in
|
|
p.outputFormat = C.HEIF
|
|
p.quality = C.int(params.Quality)
|
|
p.heifLossless = C.int(boolToInt(params.Lossless))
|
|
p.heifBitdepth = C.int(params.Bitdepth)
|
|
p.heifEffort = C.int(params.Effort)
|
|
|
|
return vipsSaveToBuffer(p)
|
|
}
|
|
|
|
func vipsSaveAVIFToBuffer(in *C.VipsImage, params AvifExportParams) ([]byte, error) {
|
|
incOpCounter("save_heif_buffer")
|
|
|
|
// Speed was deprecated but we want to avoid breaking code that still uses it:
|
|
effort := params.Effort
|
|
if params.Speed != 0 {
|
|
effort = params.Speed
|
|
}
|
|
|
|
p := C.create_save_params(C.AVIF)
|
|
p.inputImage = in
|
|
p.stripMetadata = C.int(boolToInt(params.StripMetadata))
|
|
p.outputFormat = C.AVIF
|
|
p.quality = C.int(params.Quality)
|
|
p.heifLossless = C.int(boolToInt(params.Lossless))
|
|
p.heifBitdepth = C.int(params.Bitdepth)
|
|
p.heifEffort = C.int(effort)
|
|
|
|
return vipsSaveToBuffer(p)
|
|
}
|
|
|
|
func vipsSaveJP2KToBuffer(in *C.VipsImage, params Jp2kExportParams) ([]byte, error) {
|
|
incOpCounter("save_jp2k_buffer")
|
|
|
|
p := C.create_save_params(C.JP2K)
|
|
p.inputImage = in
|
|
p.outputFormat = C.JP2K
|
|
p.quality = C.int(params.Quality)
|
|
p.jp2kLossless = C.int(boolToInt(params.Lossless))
|
|
p.jp2kTileWidth = C.int(params.TileWidth)
|
|
p.jp2kTileHeight = C.int(params.TileHeight)
|
|
p.jpegSubsample = C.VipsForeignSubsample(params.SubsampleMode)
|
|
|
|
return vipsSaveToBuffer(p)
|
|
}
|
|
|
|
func vipsSaveGIFToBuffer(in *C.VipsImage, params GifExportParams) ([]byte, error) {
|
|
incOpCounter("save_gif_buffer")
|
|
|
|
p := C.create_save_params(C.GIF)
|
|
p.inputImage = in
|
|
p.quality = C.int(params.Quality)
|
|
p.gifDither = C.double(params.Dither)
|
|
p.gifEffort = C.int(params.Effort)
|
|
p.gifBitdepth = C.int(params.Bitdepth)
|
|
|
|
return vipsSaveToBuffer(p)
|
|
}
|
|
|
|
func vipsSaveJxlToBuffer(in *C.VipsImage, params JxlExportParams) ([]byte, error) {
|
|
incOpCounter("save_jxl_buffer")
|
|
|
|
p := C.create_save_params(C.JXL)
|
|
p.inputImage = in
|
|
p.outputFormat = C.JXL
|
|
p.quality = C.int(params.Quality)
|
|
p.jxlLossless = C.int(boolToInt(params.Lossless))
|
|
p.jxlTier = C.int(params.Tier)
|
|
p.jxlDistance = C.double(params.Distance)
|
|
p.jxlEffort = C.int(params.Effort)
|
|
|
|
return vipsSaveToBuffer(p)
|
|
}
|
|
|
|
func vipsSaveToBuffer(params C.struct_SaveParams) ([]byte, error) {
|
|
if err := C.save_to_buffer(¶ms); err != 0 {
|
|
return nil, handleSaveBufferError(params.outputBuffer)
|
|
}
|
|
|
|
buf := C.GoBytes(params.outputBuffer, C.int(params.outputLen))
|
|
defer gFreePointer(params.outputBuffer)
|
|
|
|
return buf, nil
|
|
}
|