diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index e85d045c..85de180d 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -27,6 +27,7 @@ jobs: sudo add-apt-repository -y ppa:strukturag/libde265 sudo add-apt-repository -y ppa:strukturag/libheif sudo add-apt-repository -y ppa:tonimelisma/ppa + sudo apt-get -y install libopenjp2-7 sudo apt-get -y install libvips-dev - name: Install macos deps diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 1a021e10..d0332642 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -14,7 +14,8 @@ The CI/CD currently checks strict format and linting, so be sure to add comments ## Run the tests -```make test +```bash +$ make test ``` ## Get in touch diff --git a/resources/jp2k-orientation-6.jp2 b/resources/jp2k-orientation-6.jp2 new file mode 100644 index 00000000..4b57ef0e Binary files /dev/null and b/resources/jp2k-orientation-6.jp2 differ diff --git a/vips/foreign.c b/vips/foreign.c index 1c35532d..2f471165 100644 --- a/vips/foreign.c +++ b/vips/foreign.c @@ -68,7 +68,13 @@ int load_image_buffer(LoadParams *params, void *buf, size_t len, code = vips_heifload_buffer(buf, len, out, "page", params->page, "n", params->n, "thumbnail", params->heifThumbnail, "autorotate", params->autorotate, NULL); + + } + #if (VIPS_MAJOR_VERSION >= 8) && (VIPS_MINOR_VERSION >= 11) + else if (imageType == JP2K) { + code = vips_jp2kload_buffer(buf, len, out, "page", params->page, NULL); } + #endif return code; } @@ -137,6 +143,11 @@ int set_heifload_options(VipsOperation *operation, LoadParams *params) { return 0; } +int set_jp2kload_options(VipsOperation *operation, LoadParams *params) { + MAYBE_SET_INT(operation, params->page, "page"); + return 0; +} + int set_magickload_options(VipsOperation *operation, LoadParams *params) { MAYBE_SET_INT(operation, params->page, "page"); MAYBE_SET_INT(operation, params->n, "n"); @@ -266,6 +277,7 @@ int set_webpsave_options(VipsOperation *operation, SaveParams *params) { vips_object_set(VIPS_OBJECT(operation), "strip", params->stripMetadata, "lossless", params->webpLossless, + "near_lossless", params->webpNearLossless, "reduction_effort", params->webpReductionEffort, "profile", params->webpIccProfile ? params->webpIccProfile : "none", NULL); @@ -327,6 +339,19 @@ int set_avifsave_options(VipsOperation *operation, SaveParams *params) { return ret; } +int set_jp2ksave_options(VipsOperation *operation, SaveParams *params) { + int ret = vips_object_set( + VIPS_OBJECT(operation), "subsample_mode", params->jpegSubsample, + "tile_height", params->jp2kTileHeight, "tile_width", params->jp2kTileWidth, + "lossless", params->jp2kLossless, NULL); + + if (!ret && params->quality) { + ret = vips_object_set(VIPS_OBJECT(operation), "Q", params->quality, NULL); + } + + return ret; +} + int load_from_buffer(LoadParams *params, void *buf, size_t len) { switch (params->inputFormat) { case JPEG: @@ -359,6 +384,9 @@ int load_from_buffer(LoadParams *params, void *buf, size_t len) { case AVIF: return load_buffer("heifload_buffer", buf, len, params, set_heifload_options); + case JP2K: + return load_buffer("jp2kload_buffer", buf, len, params, + set_jp2kload_options); default: g_warning("Unsupported input type given: %d", params->inputFormat); } @@ -381,6 +409,8 @@ int save_to_buffer(SaveParams *params) { return save_buffer("magicksave_buffer", params, set_magicksave_options); case AVIF: return save_buffer("heifsave_buffer", params, set_avifsave_options); + case JP2K: + return save_buffer("jp2ksave_buffer", params, set_jp2ksave_options); default: g_warning("Unsupported output type given: %d", params->outputFormat); } @@ -431,6 +461,7 @@ static SaveParams defaultSaveParams = { .pngFilter = VIPS_FOREIGN_PNG_FILTER_NONE, .webpLossless = FALSE, + .webpNearLossless = FALSE, .webpReductionEffort = 4, .webpIccProfile = NULL, @@ -445,7 +476,11 @@ static SaveParams defaultSaveParams = { .tiffXRes = 1.0, .tiffYRes = 1.0, - .avifSpeed = 5}; + .avifSpeed = 5, + + .jp2kLossless = FALSE, + .jp2kTileHeight = 512, + .jp2kTileWidth = 512}; SaveParams create_save_params(ImageType outputFormat) { SaveParams params = defaultSaveParams; diff --git a/vips/foreign.go b/vips/foreign.go index 5617abec..78809e7f 100644 --- a/vips/foreign.go +++ b/vips/foreign.go @@ -43,6 +43,7 @@ const ( ImageTypeHEIF ImageType = C.HEIF ImageTypeBMP ImageType = C.BMP ImageTypeAVIF ImageType = C.AVIF + ImageTypeJP2K ImageType = C.JP2K ) var imageTypeExtensionMap = map[ImageType]string{ @@ -57,6 +58,7 @@ var imageTypeExtensionMap = map[ImageType]string{ ImageTypeHEIF: ".heic", ImageTypeBMP: ".bmp", ImageTypeAVIF: ".avif", + ImageTypeJP2K: ".jp2", } // ImageTypes defines the various image types supported by govips @@ -72,6 +74,7 @@ var ImageTypes = map[ImageType]string{ ImageTypeHEIF: "heif", ImageTypeBMP: "bmp", ImageTypeAVIF: "heif", + ImageTypeJP2K: "jp2k", } // TiffCompression represents method for compressing a tiff at export @@ -138,6 +141,8 @@ func DetermineImageType(buf []byte) ImageType { return ImageTypePDF } else if isBMP(buf) { return ImageTypeBMP + } else if isJP2K(buf) { + return ImageTypeJP2K } else { return ImageTypeUnknown } @@ -225,6 +230,14 @@ 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) +} + func vipsLoadFromBuffer(buf []byte, params *ImportParams) (*C.VipsImage, ImageType, error) { src := buf // Reference src here so it's not garbage collected during image initialization. @@ -346,6 +359,7 @@ func vipsSaveWebPToBuffer(in *C.VipsImage, params WebpExportParams) ([]byte, err 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) if params.IccProfile != "" { @@ -393,6 +407,21 @@ func vipsSaveAVIFToBuffer(in *C.VipsImage, params AvifExportParams) ([]byte, err 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.VipsForeignJpegSubsample(params.SubsampleMode) + + return vipsSaveToBuffer(p) +} + func vipsSaveGIFToBuffer(in *C.VipsImage, params GifExportParams) ([]byte, error) { incOpCounter("save_gif_buffer") diff --git a/vips/foreign.h b/vips/foreign.h index 8777b6ae..60f06467 100644 --- a/vips/foreign.h +++ b/vips/foreign.h @@ -24,7 +24,8 @@ typedef enum types { MAGICK, HEIF, BMP, - AVIF + AVIF, + JP2K } ImageType; typedef enum ParamType { @@ -97,6 +98,7 @@ typedef struct SaveParams { // WEBP BOOL webpLossless; + BOOL webpNearLossless; int webpReductionEffort; char *webpIccProfile; @@ -115,6 +117,11 @@ typedef struct SaveParams { // AVIF int avifSpeed; + + // JPEG2000 + BOOL jp2kLossless; + int jp2kTileWidth; + int jp2kTileHeight; } SaveParams; SaveParams create_save_params(ImageType outputFormat); diff --git a/vips/foreign_test.go b/vips/foreign_test.go index da48bbf9..5f96dc79 100644 --- a/vips/foreign_test.go +++ b/vips/foreign_test.go @@ -127,3 +127,14 @@ func Test_DetermineImageType__AVIF(t *testing.T) { imageType := DetermineImageType(buf) assert.Equal(t, ImageTypeAVIF, imageType) } + +func Test_DetermineImageType__JP2K(t *testing.T) { + Startup(&Config{}) + + buf, err := ioutil.ReadFile(resources + "jp2k-orientation-6.jp2") + assert.NoError(t, err) + assert.NotNil(t, buf) + + imageType := DetermineImageType(buf) + assert.Equal(t, ImageTypeJP2K, imageType) +} diff --git a/vips/image.go b/vips/image.go index 527f9c10..460b15b5 100644 --- a/vips/image.go +++ b/vips/image.go @@ -233,6 +233,7 @@ type WebpExportParams struct { StripMetadata bool Quality int Lossless bool + NearLossless bool ReductionEffort int IccProfile string } @@ -243,6 +244,7 @@ func NewWebpExportParams() *WebpExportParams { return &WebpExportParams{ Quality: 75, Lossless: false, + NearLossless: false, ReductionEffort: 4, } } @@ -307,6 +309,25 @@ func NewAvifExportParams() *AvifExportParams { } } +// Jp2kExportParams are options when exporting an JPEG2000 to file or buffer. +type Jp2kExportParams struct { + Quality int + Lossless bool + TileWidth int + TileHeight int + SubsampleMode SubsampleMode +} + +// NewJp2kExportParams creates default values for an export of an JPEG2000 image. +func NewJp2kExportParams() *Jp2kExportParams { + return &Jp2kExportParams{ + Quality: 80, + Lossless: false, + TileWidth: 512, + TileHeight: 512, + } +} + // NewImageFromReader loads an ImageRef from the given reader func NewImageFromReader(r io.Reader) (*ImageRef, error) { buf, err := ioutil.ReadAll(r) @@ -341,12 +362,12 @@ func LoadImageFromBuffer(buf []byte, params *ImportParams) (*ImageRef, error) { params = NewImportParams() } - image, format, err := vipsLoadFromBuffer(buf, params) + vipsImage, format, err := vipsLoadFromBuffer(buf, params) if err != nil { return nil, err } - ref := newImageRef(image, format, buf) + ref := newImageRef(vipsImage, format, buf) govipsLog("govips", LogLevelDebug, fmt.Sprintf("created imageref %p", ref)) return ref, nil @@ -640,6 +661,8 @@ func (r *ImageRef) ExportNative() ([]byte, *ImageMetadata, error) { return r.ExportTiff(NewTiffExportParams()) case ImageTypeAVIF: return r.ExportAvif(NewAvifExportParams()) + case ImageTypeJP2K: + return r.ExportJp2k(NewJp2kExportParams()) default: return r.ExportJpeg(NewJpegExportParams()) } @@ -746,6 +769,20 @@ func (r *ImageRef) ExportAvif(params *AvifExportParams) ([]byte, *ImageMetadata, return buf, r.newMetadata(ImageTypeAVIF), nil } +// ExportJp2k exports the image as JPEG2000 to a buffer. +func (r *ImageRef) ExportJp2k(params *Jp2kExportParams) ([]byte, *ImageMetadata, error) { + if params == nil { + params = NewJp2kExportParams() + } + + buf, err := vipsSaveJP2KToBuffer(r.image, *params) + if err != nil { + return nil, nil, err + } + + return buf, r.newMetadata(ImageTypeJP2K), nil +} + // CompositeMulti composites the given overlay image on top of the associated image with provided blending mode. func (r *ImageRef) CompositeMulti(ins []*ImageComposite) error { out, err := vipsComposite(toVipsCompositeStructs(r, ins)) @@ -834,8 +871,8 @@ func (r *ImageRef) ExtractBand(band int, num int) error { // BandJoin joins a set of images together, bandwise. func (r *ImageRef) BandJoin(images ...*ImageRef) error { vipsImages := []*C.VipsImage{r.image} - for _, image := range images { - vipsImages = append(vipsImages, image.image) + for _, vipsImage := range images { + vipsImages = append(vipsImages, vipsImage.image) } out, err := vipsBandJoin(vipsImages) @@ -1383,8 +1420,8 @@ func (r *ImageRef) ToBytes() ([]byte, error) { } defer C.free(cData) - bytes := C.GoBytes(unsafe.Pointer(cData), C.int(cSize)) - return bytes, nil + data := C.GoBytes(unsafe.Pointer(cData), C.int(cSize)) + return data, nil } func (r *ImageRef) determineInputICCProfile() (inputProfile string) { diff --git a/vips/image_test.go b/vips/image_test.go index 85137cd5..e26c862e 100644 --- a/vips/image_test.go +++ b/vips/image_test.go @@ -52,6 +52,23 @@ func TestImageRef_WebP__ReducedEffort(t *testing.T) { assert.NoError(t, err) } +func TestImageRef_WebP__NearLossless(t *testing.T) { + Startup(nil) + + srcBytes, err := ioutil.ReadFile(resources + "webp+alpha.webp") + require.NoError(t, err) + + src := bytes.NewReader(srcBytes) + img, err := NewImageFromReader(src) + require.NoError(t, err) + require.NotNil(t, img) + + params := NewWebpExportParams() + params.NearLossless = true + _, _, err = img.ExportWebp(params) + assert.NoError(t, err) +} + func TestImageRef_PNG(t *testing.T) { Startup(nil) @@ -924,6 +941,24 @@ func TestImageRef_AVIF(t *testing.T) { assert.Equal(t, ImageTypeAVIF, metadata.Format) } +func TestImageRef_JP2K(t *testing.T) { + if MajorVersion == 8 && MinorVersion < 11 { + t.Skip("JPEG2000 is only supported in vips 8.11+") + } + Startup(nil) + + raw, err := ioutil.ReadFile(resources + "jp2k-orientation-6.jp2") + require.NoError(t, err) + + img, err := NewImageFromBuffer(raw) + require.NoError(t, err) + require.NotNil(t, img) + + _, metadata, err := img.ExportJp2k(nil) + assert.NoError(t, err) + assert.Equal(t, ImageTypeJP2K, metadata.Format) +} + // TODO unit tests to cover: // NewImageFromReader failing test // NewImageFromFile failing test