diff --git a/README.md b/README.md index 9daa559..42de00e 100644 --- a/README.md +++ b/README.md @@ -8,20 +8,27 @@ A Java interface to [llibvips](http://libvips.github.io/libvips/), the fast imag io.github.codecitizen jlibvips - 1.2.1 + 1.3.0.RELEASE ``` ```groovy -implementation 'io.github.codecitizen:jlibvips:1.2.1' +implementation 'io.github.codecitizen:jlibvips:1.3.0.RELEASE' ``` **Configure Path to libvips Library:** +From code: ```java VipsBindingsSingleton.configure("/usr/local/lib/libvips.so"); ``` +You may also set the lib path in environment variable `JLIBVIPS_LIB_PATH`, which is useful +when running your app in multiple environments where lib position changes. Example for macOS: +```shell script +export JLIBVIPS_LIB_PATH="/usr/local/Cellar/vips/8.10.2_4/lib/libvips.dylib" +``` + **Example: Generate a Thumbnail for a PDF.** ```java @@ -52,7 +59,7 @@ public class PDFThumbnailExample { } ``` -**Example: Create an Image Pyramid form a large PNG File.** +**Example: Create a DZ Image Pyramid from a large PNG File.** ```java @@ -64,7 +71,7 @@ import java.nio.file.Paths; public class ImagePyramidExample { public static void main(String[] args) { - var image = VipsImage.formFile(Paths.get(args[0])); + var image = VipsImage.fromFile(Paths.get(args[0])); var directory = Files.createTempDirectory("example-pyramid"); image.deepZoom(directory) .layout(DeepZoomLayouts.Google) @@ -72,7 +79,36 @@ public class ImagePyramidExample { .suffix(".jpg[Q=100]") .save(); image.unref(); - System.out.printf("Pyramid generated in folder '%s'.%n", directory.toString()); + System.out.printf("Pyramid generated in folder '%s'.\n", directory.toString()); + System.out.println("Done."); + } + +} +``` + +**Example: Create a TIFF Image Pyramid from a large PNG File.** + +```java +package jlibvips.example; + +import org.jlibvips.VipsImage; +import java.nio.file.Paths; + +public class ImagePyramidExample { + + public static void main(String[] args) { + var image = VipsImage.fromFile(Paths.get(args[0])); + Path dest = image + .tiff() + .tile(true) + .tileHeight(256) + .tileWidth(256) + .compression(VipsForeignTiffCompression.VIPS_FOREIGN_TIFF_COMPRESSION_JPEG) + .quality(80) + .pyramid(true) + .save(); + image.unref(); + System.out.printf("Pyramid generated in file '%s'.\n", dest.toString()); System.out.println("Done."); } @@ -134,3 +170,5 @@ VIPS[G_LOG_LEVEL_INFO]: gaussblur mask width 17 VIPS[G_LOG_LEVEL_INFO]: convi: using C path VIPS[G_LOG_LEVEL_INFO]: convi: using C path ``` + +The GLIBC lib path can also be set using env variable `JLIBVIPS_GLIBC_PATH`. \ No newline at end of file diff --git a/build.gradle b/build.gradle index f1a9a86..db8be54 100644 --- a/build.gradle +++ b/build.gradle @@ -10,7 +10,7 @@ plugins { group 'io.github.codecitizen' archivesBaseName = 'jlibvips' -version '1.3.0.RELEASE' +version '1.4.0-SNAPSHOT' sourceCompatibility = 1.8 targetCompatibility = 1.8 @@ -25,10 +25,10 @@ repositories { } dependencies { - implementation group: 'net.java.dev.jna', name: 'jna', version: '5.1.0' - implementation 'org.codehaus.groovy:groovy:2.5.4' + implementation group: 'net.java.dev.jna', name: 'jna', version: '5.6.0' + implementation 'org.codehaus.groovy:groovy:2.5.13' - testCompile 'org.spockframework:spock-core:1.2-groovy-2.5' + testCompile 'org.spockframework:spock-core:1.3-groovy-2.5' } jar { @@ -57,7 +57,7 @@ javadoc { } wrapper { - gradleVersion = '5.0' + gradleVersion = '6.7.1' } artifacts { diff --git a/src/main/java/org/jlibvips/VipsAngle.java b/src/main/java/org/jlibvips/VipsAngle.java index e4b8995..be4dce1 100644 --- a/src/main/java/org/jlibvips/VipsAngle.java +++ b/src/main/java/org/jlibvips/VipsAngle.java @@ -26,7 +26,7 @@ public static VipsAngle fromInteger(int value) { case 180: return D180; case 270: return D270; default: - throw new IllegalArgumentException("Allowed VipsAngle's are [0°, 90°, 180°, 270°]."); + throw new IllegalArgumentException("Allowed VipsAngle's are [0, 90, 180, 270]."); } } diff --git a/src/main/java/org/jlibvips/VipsForeignDzDepth.java b/src/main/java/org/jlibvips/VipsForeignDzDepth.java new file mode 100644 index 0000000..ac58227 --- /dev/null +++ b/src/main/java/org/jlibvips/VipsForeignDzDepth.java @@ -0,0 +1,11 @@ +package org.jlibvips; + +/** + * https://libvips.github.io/libvips/API/current/VipsForeignSave.html#VipsForeignDzDepth + */ +public enum VipsForeignDzDepth { + VIPS_FOREIGN_DZ_DEPTH_ONEPIXEL, + VIPS_FOREIGN_DZ_DEPTH_ONETILE, + VIPS_FOREIGN_DZ_DEPTH_ONE, + VIPS_FOREIGN_DZ_DEPTH_LAST +} diff --git a/src/main/java/org/jlibvips/VipsForeignTiffCompression.java b/src/main/java/org/jlibvips/VipsForeignTiffCompression.java new file mode 100644 index 0000000..8dfaef5 --- /dev/null +++ b/src/main/java/org/jlibvips/VipsForeignTiffCompression.java @@ -0,0 +1,13 @@ +package org.jlibvips; + +public enum VipsForeignTiffCompression { + VIPS_FOREIGN_TIFF_COMPRESSION_NONE, + VIPS_FOREIGN_TIFF_COMPRESSION_JPEG, + VIPS_FOREIGN_TIFF_COMPRESSION_DEFLATE, + VIPS_FOREIGN_TIFF_COMPRESSION_PACKBITS, + VIPS_FOREIGN_TIFF_COMPRESSION_CCITTFAX4, + VIPS_FOREIGN_TIFF_COMPRESSION_LZW, + VIPS_FOREIGN_TIFF_COMPRESSION_WEBP, + VIPS_FOREIGN_TIFF_COMPRESSION_ZSTD, + VIPS_FOREIGN_TIFF_COMPRESSION_LAST +} diff --git a/src/main/java/org/jlibvips/VipsForeignTiffPredictor.java b/src/main/java/org/jlibvips/VipsForeignTiffPredictor.java new file mode 100644 index 0000000..1dca832 --- /dev/null +++ b/src/main/java/org/jlibvips/VipsForeignTiffPredictor.java @@ -0,0 +1,11 @@ +package org.jlibvips; + +/** + * https://libvips.github.io/libvips/API/current/VipsForeignSave.html#VipsForeignTiffPredictor + */ +public enum VipsForeignTiffPredictor { + VIPS_FOREIGN_TIFF_PREDICTOR_NONE, + VIPS_FOREIGN_TIFF_PREDICTOR_HORIZONTAL, + VIPS_FOREIGN_TIFF_PREDICTOR_FLOAT, + VIPS_FOREIGN_TIFF_PREDICTOR_LAST +} diff --git a/src/main/java/org/jlibvips/VipsForeignTiffResunit.java b/src/main/java/org/jlibvips/VipsForeignTiffResunit.java new file mode 100644 index 0000000..c89edab --- /dev/null +++ b/src/main/java/org/jlibvips/VipsForeignTiffResunit.java @@ -0,0 +1,10 @@ +package org.jlibvips; + +/** + * https://libvips.github.io/libvips/API/current/VipsForeignSave.html#VipsForeignTiffResunit + */ +public enum VipsForeignTiffResunit { + VIPS_FOREIGN_TIFF_RESUNIT_CM, + VIPS_FOREIGN_TIFF_RESUNIT_INCH, + VIPS_FOREIGN_TIFF_RESUNIT_LAST +} diff --git a/src/main/java/org/jlibvips/VipsImage.java b/src/main/java/org/jlibvips/VipsImage.java index c522c85..e3d25bd 100644 --- a/src/main/java/org/jlibvips/VipsImage.java +++ b/src/main/java/org/jlibvips/VipsImage.java @@ -388,6 +388,21 @@ public PngSaveOperation png() { return new PngSaveOperation(this); } + /** + * Write an image to a file in TIFF format. + * + * + * java.nio.Path path = image.tiff().save(); + * + * + * vips_tiffsave() + * + * @return the {@link TiffSaveOperation} + */ + public TiffSaveOperation tiff() { + return new TiffSaveOperation(this); + } + /** * Get the width of this image. * diff --git a/src/main/java/org/jlibvips/VipsRegionShrink.java b/src/main/java/org/jlibvips/VipsRegionShrink.java new file mode 100644 index 0000000..d670cb0 --- /dev/null +++ b/src/main/java/org/jlibvips/VipsRegionShrink.java @@ -0,0 +1,14 @@ +package org.jlibvips; + +/** + * https://libvips.github.io/libvips/API/current/VipsRegion.html#VipsRegionShrink + */ +public enum VipsRegionShrink { + VIPS_REGION_SHRINK_MEAN, + VIPS_REGION_SHRINK_MEDIAN, + VIPS_REGION_SHRINK_MODE, + VIPS_REGION_SHRINK_MAX, + VIPS_REGION_SHRINK_MIN, + VIPS_REGION_SHRINK_NEAREST, + VIPS_REGION_SHRINK_LAST +} diff --git a/src/main/java/org/jlibvips/jna/VipsBindings.java b/src/main/java/org/jlibvips/jna/VipsBindings.java index 8a05cd6..69455dc 100644 --- a/src/main/java/org/jlibvips/jna/VipsBindings.java +++ b/src/main/java/org/jlibvips/jna/VipsBindings.java @@ -25,6 +25,7 @@ public interface VipsBindings extends Library { int vips_jpegsave(Pointer in, String filename, Object...args); int vips_webpsave(Pointer in, String filename, Object...args); int vips_pngsave(Pointer in, String filename, Object...args); + int vips_tiffsave(Pointer in, String filename, Object...args); int vips_insert(Pointer main, Pointer sub, Pointer[] out, int x, int y, Object...args); int vips_join(Pointer in1, Pointer in2, Pointer[] out, int direction, Object...args); diff --git a/src/main/java/org/jlibvips/jna/VipsBindingsSingleton.java b/src/main/java/org/jlibvips/jna/VipsBindingsSingleton.java index b3cd256..b896623 100644 --- a/src/main/java/org/jlibvips/jna/VipsBindingsSingleton.java +++ b/src/main/java/org/jlibvips/jna/VipsBindingsSingleton.java @@ -4,8 +4,8 @@ public class VipsBindingsSingleton { - - private static String libraryPath = "vips"; + private static final String ENV_LIB_PATH = "JLIBVIPS_LIB_PATH"; + private static String libraryPath = System.getenv(ENV_LIB_PATH) == null ? "vips" : System.getenv(ENV_LIB_PATH); public static void configure(String lp) { libraryPath = lp; @@ -16,9 +16,13 @@ public static void configure(String lp) { public static VipsBindings instance() { if(INSTANCE == null) { if(libraryPath == null || libraryPath.isEmpty()) { - throw new IllegalStateException("Please call VipsBindingsSingleton.configure(...) before getting the instance."); + throw new IllegalStateException("Please call VipsBindingsSingleton.configure(...) or set env var JLIBVIPS_LIB_PATH before getting the instance."); + } + try { + INSTANCE = Native.load(libraryPath, VipsBindings.class); + } catch (UnsatisfiedLinkError e) { + throw new IllegalStateException("Please call VipsBindingsSingleton.configure(...) or set env var JLIBVIPS_LIB_PATH before getting the instance."); } - INSTANCE = Native.load(libraryPath, VipsBindings.class); } return INSTANCE; } diff --git a/src/main/java/org/jlibvips/jna/glib/GLibBindingsSingleton.java b/src/main/java/org/jlibvips/jna/glib/GLibBindingsSingleton.java index d955916..839ce52 100644 --- a/src/main/java/org/jlibvips/jna/glib/GLibBindingsSingleton.java +++ b/src/main/java/org/jlibvips/jna/glib/GLibBindingsSingleton.java @@ -4,7 +4,10 @@ public class GLibBindingsSingleton { - private static String libraryPath = "/usr/local/opt/glib/lib/libglib-2.0.dylib"; + private static final String ENV_GLIBC_PATH = "JLIBVIPS_GLIBC_PATH"; + private static String libraryPath = System.getenv(ENV_GLIBC_PATH) == null + ? "libglib-2.0" + : System.getenv(ENV_GLIBC_PATH); public static void configure(String lp) { libraryPath = lp; diff --git a/src/main/java/org/jlibvips/operations/TiffSaveOperation.java b/src/main/java/org/jlibvips/operations/TiffSaveOperation.java new file mode 100644 index 0000000..6031741 --- /dev/null +++ b/src/main/java/org/jlibvips/operations/TiffSaveOperation.java @@ -0,0 +1,338 @@ +package org.jlibvips.operations; + +import org.jlibvips.*; +import org.jlibvips.exceptions.VipsException; +import org.jlibvips.jna.VipsBindingsSingleton; +import org.jlibvips.util.Varargs; +import org.jlibvips.util.VipsUtils; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; + +/** + * Write a VIPS image to a file as TIFF. + */ +public class TiffSaveOperation implements SaveOperation { + + private final VipsImage image; + + private Integer quality; + private VipsForeignTiffCompression tiffCompression; + private Boolean tile; + private Integer tileWidth; + private Integer tileHeight; + private Boolean pyramid; + private VipsForeignTiffPredictor predictor; + private String profile; + private Integer bitdepth; + private Boolean miniswhite; + private VipsForeignTiffResunit resunit; + private Double xres; + private Double yres; + private Boolean bigtiff; + private Boolean properties; + private VipsRegionShrink region_shrink; + private VipsForeignDzDepth depth; + private Integer level; + private Boolean lossless; + private Boolean subifd; + + public TiffSaveOperation(VipsImage image) { + this.image = image; + } + + @Override + public Path save() throws IOException, VipsException { + if (tileWidth % 128 != 0 || tileHeight % 128 != 0) { + throw new VipsException("Wrong value for tileWidth or TileHeight", 1); + } + Path path = Files.createTempFile("jlibvips", ".tif"); + int ret = VipsBindingsSingleton.instance().vips_tiffsave(image.getPtr(), path.toString(), + new Varargs().add("Q", quality) + .add("compression", VipsUtils.toOrdinal(tiffCompression)) + .add("tile", VipsUtils.booleanToInteger(tile)) + .add("tile_width", tileWidth) + .add("tile_height", tileHeight) + .add("pyramid", VipsUtils.booleanToInteger(pyramid)) + .add("predictor", VipsUtils.toOrdinal(predictor)) + .add("profile", profile) + .add("bitdepth", bitdepth) + .add("miniswhite", VipsUtils.booleanToInteger(miniswhite)) + .add("resunit", VipsUtils.toOrdinal(resunit)) + .add("xres", xres) + .add("yres", yres) + .add("bigtiff", VipsUtils.booleanToInteger(bigtiff)) + .add("properties", VipsUtils.booleanToInteger(properties)) + .add("region_shrink", VipsUtils.toOrdinal(region_shrink)) + .add("depth", VipsUtils.toOrdinal(depth)) + .add("level", level) + .add("lossless", VipsUtils.booleanToInteger(lossless)) + .add("subifd", VipsUtils.booleanToInteger(subifd)) + .toArray()); + if (ret != 0) { + throw new VipsException("vips_tiffsave", ret); + } + return path; + } + + /** + * Set the JPEG compression factor. Default 75. + * + * @param q quality factor + * @return this + */ + public TiffSaveOperation quality(Integer q) { + this.quality = q; + return this; + } + + /** + * Use compression to set the tiff compression. Currently jpeg, packbits, fax4, + * lzw, none, deflate, webp and zstd are supported. The default is no compression. + * JPEG compression is a good lossy compressor for photographs, packbits is good + * for 1-bit images, and deflate is the best lossless compression TIFF can do. + * + * @param compression compression ENUM + * @return this + */ + public TiffSaveOperation compression(VipsForeignTiffCompression compression) { + this.tiffCompression = compression; + return this; + } + + /** + * Set true to write a tiled tiff + * + * @param tile true or false + * @return this + */ + public TiffSaveOperation tile(boolean tile) { + this.tile = tile; + return this; + } + + /** + * Set tile width, must be 2^N, i.e. 128, 256, 512 etc + * Default is 128. + * + * @param tileWidth width of tiles + * @return this + */ + public TiffSaveOperation tileWidth(int tileWidth) { + this.tileWidth = tileWidth; + return this; + } + + /** + * Set tile width, must be 2^N, i.e. 128, 256, 512 etc + * Default is 128. + * + * @param tileHeight height of tiles + * @return this + */ + public TiffSaveOperation tileHeight(int tileHeight) { + this.tileHeight = tileHeight; + return this; + } + + /** + * Set pyramid to write the image as a set of images, one per page, of decreasing size. + * By default, the pyramid stops when the image is small enough to fit in one tile. + * Use depth to stop when the image fits in one pixel, or to only write a single layer. + * + * @param pyramid true or false + * @return this + */ + public TiffSaveOperation pyramid(boolean pyramid) { + this.pyramid = pyramid; + return this; + } + + /** + * Use predictor to set the predictor for lzw and deflate compression. + * It defaults to VIPS_FOREIGN_TIFF_PREDICTOR_HORIZONTAL, meaning horizontal + * differencing. Please refer to the libtiff specifications for further + * discussion of various predictors. + * + * @param predictor enum + * @return this + */ + public TiffSaveOperation predictor(VipsForeignTiffPredictor predictor) { + this.predictor = predictor; + return this; + } + + /** + * Use profile to give the filename of a profile to be embedded in the TIFF. + * This does not affect the pixels which are written, just the way they are tagged. + * See vips_profile_load() for details on profile naming. + *

+ * If no profile is specified and the VIPS header contains an ICC profile named + * VIPS_META_ICC_NAME, the profile from the VIPS header will be attached. + * + * @param profile name of profile + * @return this + */ + public TiffSaveOperation profile(String profile) { + this.profile = profile; + return this; + } + + /** + * Set bitdepth to save 8-bit uchar images as 1, 2 or 4-bit TIFFs. In case of + * depth 1: Values >128 are written as white, values <=128 as black. Normally + * vips will write MINISBLACK TIFFs where black is a 0 bit, but if you set + * miniswhite, it will use 0 for a white bit. Many pre-press applications + * only work with images which use this sense. miniswhite only affects one-bit images, + * it does nothing for greyscale images. In case of depth 2: The same holds + * but values < 64 are written as black. For 64 <= values < 128 they are written + * as dark grey, for 128 <= values < 192 they are written as light gray and values + * above are written as white. In case miniswhite is set to true this behavior is + * inverted. In case of depth 4: values < 16 are written as black, and so on for + * the lighter shades. In case miniswhite is set to true this behavior is inverted. + * + * @param bitdepth the bit depth + * @return this + */ + public TiffSaveOperation bitdepth(Integer bitdepth) { + this.bitdepth = bitdepth; + return this; + } + + /** + * Normally vips will write MINISBLACK TIFFs where black is a 0 bit, but if you set + * miniswhite , it will use 0 for a white bit. Many pre-press applications only work + * with images which use this sense. miniswhite only affects one-bit images, it does + * nothing for greyscale images. + * + * @param miniswhite true or false + * @return this + */ + public TiffSaveOperation miniswhite(boolean miniswhite) { + this.miniswhite = miniswhite; + return this; + } + + /** + * Use xres and yres to override the default horizontal and vertical resolutions. + * By default these values are taken from the VIPS image header. libvips resolution + * is always in pixels per millimetre. + * + * @param xres horizontal resolution in pixels/mm + * @return this + */ + public TiffSaveOperation xres(Double xres) { + this.xres = xres; + return this; + } + + /** + * Use xres and yres to override the default horizontal and vertical resolutions. + * By default these values are taken from the VIPS image header. libvips resolution + * is always in pixels per millimetre. + * + * @param yres horizontal resolution in pixels/mm + * @return this + */ + public TiffSaveOperation yres(Double yres) { + this.yres = yres; + return this; + } + + /** + * Set bigtiff to attempt to write a bigtiff. Bigtiff is a variant of the + * TIFF format that allows more than 4GB in a file. + * + * @param bigtiff true or false + * @return this + */ + public TiffSaveOperation bigtiff(boolean bigtiff) { + this.bigtiff = bigtiff; + return this; + } + + /** + * Set properties to write all vips metadata to the IMAGEDESCRIPTION tag as xml. + * If properties is not set, the value of VIPS_META_IMAGEDESCRIPTION is used instead. + * + * @param properties set TRUE to write an IMAGEDESCRIPTION tag + * @return this + */ + public TiffSaveOperation properties(boolean properties) { + this.properties = properties; + return this; + } + + /** + * Use region_shrink to set how images will be shrunk when generating pyramid: + * by default each 2x2 block is just averaged, but you can set MODE or MEDIAN as well. + * + * @param region_shrink How to shrink each 2x2 region. + * @return this + */ + public TiffSaveOperation region_shrink(VipsRegionShrink region_shrink) { + this.region_shrink = region_shrink; + return this; + } + + /** + * By default, the pyramid stops when the image is small enough to fit in one tile. + * Use depth to stop when the image fits in one pixel, or to only write a single layer. + * + * @param depth how deep to make the pyramid + * @return this + */ + public TiffSaveOperation depth(VipsForeignDzDepth depth) { + this.depth = depth; + return this; + } + + /** + * Use level to set the ZSTD compression level. + * + * @param level Zstd compression level + * @return this + */ + public TiffSaveOperation level(Integer level) { + this.level = level; + return this; + } + + /** + * Use lossless to set WEBP lossless mode on + * + * @param lossless WebP losssless mode + * @return this + */ + public TiffSaveOperation lossless(boolean lossless) { + this.lossless = lossless; + return this; + } + + /** + * Set subifd to save pyramid layers as sub-directories of the main image. + * Setting this option can improve compatibility with formats like OME. + * + * @param subifd set TRUE to write pyr layers as sub-ifds + * @return this + */ + public TiffSaveOperation subifd(boolean subifd) { + this.subifd = subifd; + return this; + } + + /** + * Use resunit to override the default resolution unit. + * The default resolution unit is taken from the header field + * VIPS_META_RESOLUTION_UNIT. If this field is not set, then + * VIPS defaults to cm. + * + * @param resunit set resolution unit + * @return this + */ + public TiffSaveOperation resunit(VipsForeignTiffResunit resunit) { + this.resunit = resunit; + return this; + } +} diff --git a/src/test/groovy/org/jlibvips/Drawing.groovy b/src/test/groovy/org/jlibvips/Drawing.groovy index 924f100..c474ff6 100644 --- a/src/test/groovy/org/jlibvips/Drawing.groovy +++ b/src/test/groovy/org/jlibvips/Drawing.groovy @@ -4,7 +4,6 @@ package org.jlibvips import spock.lang.Specification import java.nio.file.Files -import java.nio.file.Paths import java.nio.file.StandardCopyOption import static org.jlibvips.TestUtils.copyResourceToFS @@ -65,6 +64,7 @@ class Drawing extends Specification { def image = VipsImage.fromFile imagePath def whiteRgb = color(255, 255, 255) def transparent = color(0, 0, 0, 0) + def dir = Files.createTempDirectory("tint") when: def lut = VipsImage.identity().create() lut = lut.moreEq(200).ifthenelse(lut.newFromImage(transparent), lut.newFromImage(tint)) @@ -77,7 +77,7 @@ class Drawing extends Specification { then: tinted != null println tinted - def dest = Paths.get("/Volumes/HD/backups/images/${colorName}.png") + def dest = dir.resolve( "${colorName}.png") Files.move(tinted.png().save(), dest, StandardCopyOption.REPLACE_EXISTING) cleanup: Files.deleteIfExists imagePath