diff --git a/.github/docker/linux/Dockerfile b/.github/docker/linux/Dockerfile
index 18b071ff..0a98dc94 100644
--- a/.github/docker/linux/Dockerfile
+++ b/.github/docker/linux/Dockerfile
@@ -34,6 +34,8 @@ RUN yum install ${YUM_OPTIONS} \
automake \
libtool \
diffutils \
+ gperf \
+ gettext-devel \
openssl-devel \
expat-devel \
zlib-devel \
@@ -42,6 +44,7 @@ RUN yum install ${YUM_OPTIONS} \
mpfr-devel \
gmp-devel \
libmpc-devel \
+ pango-devel \
gtk-doc \
gobject-introspection gobject-introspection-devel \
glib2.x86_64 glib2-devel.x86_64 \
diff --git a/.github/docker/windows/Dockerfile b/.github/docker/windows/Dockerfile
index 9f66f0c1..73809e21 100644
--- a/.github/docker/windows/Dockerfile
+++ b/.github/docker/windows/Dockerfile
@@ -1,4 +1,4 @@
-FROM fedora:32
+FROM fedora:33
# Set default build arguments.
ARG NODE_VERSION=10.x
@@ -9,7 +9,7 @@ ARG UID=1000
ARG GID=1000
# Set default environment variables.
-ENV JAVA_HOME=/usr/lib/jvm/java-openjdk
+ENV JAVA_HOME=/usr/lib/jvm/java-1.8.0-openjdk
ENV PATH="${OSX_CROSS_HOME}/bin:${PATH}"
ENV YUM_OPTIONS="-y --setopt=skip_missing_names_on_install=False"
@@ -33,6 +33,8 @@ RUN yum install ${YUM_OPTIONS} \
automake \
libtool \
diffutils \
+ gperf \
+ gettext-devel \
openssl-devel \
expat-devel \
zlib-devel \
@@ -44,13 +46,17 @@ RUN yum install ${YUM_OPTIONS} \
gtk-doc \
gobject-introspection gobject-introspection-devel \
glib2.x86_64 glib2-devel.x86_64 \
- java-1.8.0-openjdk \
+ java-1.8.0-openjdk-devel \
mingw-w64-tools \
mingw64-gcc \
mingw64-gcc-c++ \
mingw64-glib2 \
mingw64-win-iconv \
- mingw64-expat
+ mingw64-expat \
+ mingw64-pango
+
+RUN alternatives --install "/usr/bin/java" "java" "${JAVA_HOME}/bin/java" 1
+RUN alternatives --set java ${JAVA_HOME}/bin/java
# Link the system version of libmpfr, which is more recent than expected, but works fine.
RUN ln -s /lib64/libmpfr.so.6 /lib64/libmpfr.so.4
diff --git a/CMakeLists.txt b/CMakeLists.txt
index c98392b0..53eb27f4 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -14,7 +14,7 @@ include(UseJava)
SET (CMAKE_C_COMPILER_WORKS 1)
SET (CMAKE_CXX_COMPILER_WORKS 1)
-set(CMAKE_C_FLAGS "-std=c99")
+set(CMAKE_C_FLAGS "-std=gnu99")
set(CMAKE_C_FLAGS_DEBUG "${CMAKE_C_FLAGS} -g3")
set(CMAKE_C_FLAGS_RELEASE "${CMAKE_C_FLAGS} -O3 -g")
set(CMAKE_CXX_FLAGS "-std=c++14")
diff --git a/lib/CMakeLists.txt b/lib/CMakeLists.txt
index 76e2358b..22ac805c 100644
--- a/lib/CMakeLists.txt
+++ b/lib/CMakeLists.txt
@@ -1,5 +1,8 @@
include(ExternalProject)
+find_program(MESON meson REQUIRED)
+find_program(NINJA ninja REQUIRED)
+
# Read external project versions
file(STRINGS ${CMAKE_CURRENT_SOURCE_DIR}/VERSIONS VERSIONS_LIST)
foreach(ITEM ${VERSIONS_LIST})
@@ -20,8 +23,8 @@ elseif(${BUILD_TARGET} STREQUAL "w64")
endif()
if (NOT DEFINED CMAKE_BUILD_TYPE)
-set(CONFIGURE_CFLAGS "${CMAKE_C_FLAGS}")
-set(CONFIGURE_CXXFLAGS "${CMAKE_CXX_FLAGS}")
+ set(CONFIGURE_CFLAGS "${CMAKE_C_FLAGS}")
+ set(CONFIGURE_CXXFLAGS "${CMAKE_CXX_FLAGS}")
elseif (${CMAKE_BUILD_TYPE} STREQUAL "Release")
set(CONFIGURE_CFLAGS ${CMAKE_C_FLAGS_RELEASE})
set(CONFIGURE_CXXFLAGS ${CMAKE_CXX_FLAGS_RELEASE})
@@ -44,6 +47,20 @@ list(APPEND CONFIGURE_VARS
${CONFIGURE_HOST}
)
+list(APPEND MESON_VARS
+ --bindir=${EXT_INSTALL_DIR}/bin
+ --libdir=${EXT_INSTALL_DIR}/lib
+ --includedir=${EXT_INSTALL_DIR}/include
+ --datadir=${EXT_INSTALL_DIR}/share
+ --prefix=${EXT_INSTALL_DIR}
+ )
+
+if (NOT DEFINED BUILD_TARGET)
+ set(MESON_CROSS_FILE "")
+elseif(${BUILD_TARGET} STREQUAL "w64")
+ set(MESON_CROSS_FILE --cross-file ${PROJECT_SOURCE_DIR}/meson/x86_64-w64-mingw32-crossfile.txt)
+endif()
+
find_library(LIBIMAGEQUANT imagequant PATHS "${EXT_INSTALL_DIR}/lib" NO_DEFAULT_PATH)
if (NOT LIBIMAGEQUANT)
# https://github.com/ImageOptim/libimagequant/issues/36
@@ -74,6 +91,74 @@ else()
add_custom_target(libimagequant "")
endif()
+find_library(FREETYPE freetype PATHS "${EXT_INSTALL_DIR}/lib" NO_DEFAULT_PATH)
+if(NOT FREETYPE)
+ ExternalProject_Add(freetype
+ URL "http://download.savannah.nongnu.org/releases/freetype/freetype-${FREETYPE_VERSION}.tar.gz"
+ PREFIX "${CMAKE_CURRENT_BINARY_DIR}/freetype"
+ CMAKE_ARGS
+ -DCMAKE_INSTALL_PREFIX=${EXT_INSTALL_DIR}
+ -DCMAKE_TOOLCHAIN_FILE=${CMAKE_TOOLCHAIN_FILE}
+ -DCMAKE_BUILD_TYPE=Release
+ -DBUILD_SHARED_LIBS=1
+ -DENABLE_CCACHE=0
+ )
+else()
+ add_custom_target(freetype "")
+endif()
+
+find_library(HARFBUZZ harfbuzz PATHS "${EXT_INSTALL_DIR}/lib" NO_DEFAULT_PATH)
+if(NOT HARFBUZZ)
+ ExternalProject_Add(harfbuzz
+ URL "https://github.com/harfbuzz/harfbuzz/releases/download/${HARFBUZZ_VERSION}/harfbuzz-${HARFBUZZ_VERSION}.tar.xz"
+ PREFIX "${CMAKE_CURRENT_BINARY_DIR}/harfbuzz"
+ CONFIGURE_COMMAND ${CMAKE_CURRENT_BINARY_DIR}/harfbuzz/src/harfbuzz/configure
+ ${CONFIGURE_VARS}
+ --enable-shared=yes
+ --enable-static=no
+ --disable-gtk-doc
+ --disable-gtk-doc-html
+ --disable-gtk-doc-pdf
+ --with-icu=no
+ --enable-introspection=no
+ --with-freetype=yes
+ DEPENDS freetype
+ )
+else()
+ add_custom_target(harfbuzz "")
+endif()
+
+find_library(FRIBIDI fribidi PATHS "${EXT_INSTALL_DIR}/lib" NO_DEFAULT_PATH)
+if(NOT FRIBIDI)
+ ExternalProject_Add(fribidi
+ URL "https://github.com/fribidi/fribidi/releases/download/v${FRIBIDI_VERSION}/fribidi-${FRIBIDI_VERSION}.tar.xz"
+ PREFIX "${CMAKE_CURRENT_BINARY_DIR}/fribidi"
+ CONFIGURE_COMMAND ${CMAKE_CURRENT_BINARY_DIR}/fribidi/src/fribidi/configure
+ ${CONFIGURE_VARS}
+ --enable-shared
+ --disable-static
+ --disable-docs
+ )
+else()
+ add_custom_target(fribidi "")
+endif()
+
+find_library(PIXMAN pixman-1 PATHS "${EXT_INSTALL_DIR}/lib" NO_DEFAULT_PATH)
+if(NOT PIXMAN)
+ ExternalProject_Add(pixman
+ URL "http://www.cairographics.org/releases/pixman-${PIXMAN_VERSION}.tar.gz"
+ PREFIX "${CMAKE_CURRENT_BINARY_DIR}/pixman"
+ CONFIGURE_COMMAND ${CMAKE_CURRENT_BINARY_DIR}/pixman/src/pixman/configure
+ ${CONFIGURE_VARS}
+ --enable-shared
+ --disable-static
+ --disable-docs
+ --disable-gtk
+ )
+else()
+ add_custom_target(pixman "")
+endif()
+
find_library(LIBEXIF exif PATHS "${EXT_INSTALL_DIR}/lib" NO_DEFAULT_PATH)
if(NOT LIBEXIF)
ExternalProject_Add(libexif
@@ -169,6 +254,69 @@ else()
add_custom_target(libpng "")
endif()
+find_library(FONTCONFIG fontconfig PATHS "${EXT_INSTALL_DIR}/lib" NO_DEFAULT_PATH)
+if(NOT FONTCONFIG)
+ ExternalProject_Add(fontconfig
+ URL "https://github.com/freedesktop/fontconfig/archive/refs/tags/${FONTCONFIG_VERSION}.tar.gz"
+ PREFIX "${CMAKE_CURRENT_BINARY_DIR}/fontconfig"
+ CONFIGURE_COMMAND ./autogen.sh
+ ${CONFIGURE_VARS}
+ --enable-shared
+ --disable-static
+ --disable-docs
+ --disable-nls
+ DEPENDS freetype
+ BUILD_IN_SOURCE 1
+ )
+else()
+ add_custom_target(fontconfig "")
+endif()
+
+find_library(CAIRO cairo PATHS "${EXT_INSTALL_DIR}/lib" NO_DEFAULT_PATH)
+if(NOT CAIRO)
+ if (NOT DEFINED BUILD_TARGET)
+ set(CAIRO_XLIB_FLAG "--enable-xlib")
+ set(CAIRO_CROSS_TARGET_FLAG "")
+ elseif(${BUILD_TARGET} STREQUAL "w64")
+ set(CAIRO_XLIB_FLAG "--disable-xlib")
+ set(CAIRO_CROSS_TARGET_FLAG "--enable-win32")
+ endif()
+ ExternalProject_Add(cairo
+ URL "http://www.cairographics.org/releases/cairo-${CAIRO_VERSION}.tar.xz"
+ PREFIX "${CMAKE_CURRENT_BINARY_DIR}/cairo"
+ CONFIGURE_COMMAND ${CMAKE_CURRENT_BINARY_DIR}/cairo/src/cairo/configure
+ ${CONFIGURE_VARS}
+ --enable-shared
+ --disable-static
+ --disable-docs
+ --disable-gl
+ --disable-xcb
+ --without-x
+ --disable-ps
+ ${CAIRO_CROSS_TARGET_FLAGS}
+ DEPENDS libpng freetype pixman fontconfig
+ )
+else()
+ add_custom_target(cairo "")
+endif()
+
+find_library(PANGO pango-1.0 PATHS "${EXT_INSTALL_DIR}/lib" NO_DEFAULT_PATH)
+if(NOT PANGO)
+ ExternalProject_Add(pango
+ URL "https://gitlab.gnome.org/GNOME/pango/-/archive/${PANGO_VERSION}/pango-${PANGO_VERSION}.tar.gz"
+ PREFIX "${CMAKE_CURRENT_BINARY_DIR}/pango"
+ CONFIGURE_COMMAND meson ${CMAKE_CURRENT_BINARY_DIR}/pango/buildir ${MESON_CROSS_FILE}
+ ${MESON_VARS}
+ -Dintrospection=disabled
+ BUILD_COMMAND ninja -C ${CMAKE_CURRENT_BINARY_DIR}/pango/buildir
+ INSTALL_COMMAND ninja -C ${CMAKE_CURRENT_BINARY_DIR}/pango/buildir install
+ BUILD_IN_SOURCE 1
+ DEPENDS cairo freetype harfbuzz fribidi
+ )
+else()
+ add_custom_target(pango "")
+endif()
+
find_library(GIFLIB gif PATHS "${EXT_INSTALL_DIR}/lib" NO_DEFAULT_PATH)
if (NOT GIFLIB)
# giflib hasn't a standard build system, don't append CONFIGURE_VARS
@@ -303,7 +451,7 @@ if(NOT VIPS)
--without-rsvg
${LIBSPNG_FLAGS}
${LIBHEIF_FLAGS}
- DEPENDS libjpeg libpng libspng giflib libwebp libimagequant lcms2 libheif tiff
+ DEPENDS libjpeg libpng libspng giflib libwebp libimagequant lcms2 libheif tiff pango
BUILD_IN_SOURCE 1
)
else()
diff --git a/lib/VERSIONS b/lib/VERSIONS
index e45436c4..eeb19756 100644
--- a/lib/VERSIONS
+++ b/lib/VERSIONS
@@ -9,4 +9,11 @@ AOM_VERSION=2.0.0
HEIF_VERSION=1.9.1
LCMS2_VERSION=2.11
TIFF_VERSION=4.1.0
+FREETYPE_VERSION=2.12.1
+HARFBUZZ_VERSION=2.7.2
+FRIBIDI_VERSION=1.0.10
+PIXMAN_VERSION=0.40.0
+FONTCONFIG_VERSION=2.14.0
+CAIRO_VERSION=1.16.0
+PANGO_VERSION=1.50.7
VIPS_VERSION=8.12.2
diff --git a/meson/x86_64-w64-mingw32-crossfile.txt b/meson/x86_64-w64-mingw32-crossfile.txt
new file mode 100644
index 00000000..176e3a3f
--- /dev/null
+++ b/meson/x86_64-w64-mingw32-crossfile.txt
@@ -0,0 +1,24 @@
+# See: https://github.com/mesonbuild/meson/blob/32c22ec492fb471dc0c1bfdbb83404a486e4a72a/cross/linux-mingw-w64-64bit.txt
+
+[binaries]
+c = '/usr/bin/x86_64-w64-mingw32-gcc'
+cpp = '/usr/bin/x86_64-w64-mingw32-g++'
+ranlib = '/usr/bin/x86_64-w64-mingw32-ranlib'
+nm = '/usr/bin/x86_64-w64-mingw32-nm'
+ld = '/usr/bin/x86_64-w64-mingw32-ld'
+objdump = '/usr/bin/x86_64-w64-mingw32-objdump'
+ar = '/usr/bin/x86_64-w64-mingw32-ar'
+strip = '/usr/bin/x86_64-w64-mingw32-strip'
+pkgconfig = '/usr/bin/x86_64-w64-mingw32-pkg-config'
+windres = '/usr/bin/x86_64-w64-mingw32-windres'
+
+[properties]
+# Directory that contains 'bin', 'lib', etc
+root = '/usr/x86_64-w64-mingw32'
+needs_exe_wrapper = True
+
+[host_machine]
+system = 'windows'
+cpu_family = 'x86_64'
+cpu = 'x86_64'
+endian = 'little'
diff --git a/src/main/c/VipsImage.c b/src/main/c/VipsImage.c
index d9f1def9..08bccada 100644
--- a/src/main/c/VipsImage.c
+++ b/src/main/c/VipsImage.c
@@ -817,6 +817,38 @@ JNICALL Java_com_criteo_vips_VipsImage_removeAutorotAngle(JNIEnv *env, jobject i
vips_autorot_remove_angle(im);
}
+JNIEXPORT jobject
+JNICALL Java_com_criteo_vips_VipsImage_textNative(JNIEnv *env, jclass cls, jstring text, jstring font, jint width,
+ jint height, jint align, jboolean justify, jint dpi, jint spacing,
+ jstring fontfile, jboolean rgba)
+{
+ VipsImage *out = NULL;
+ const char *_text = NULL;
+ const char *_font = NULL;
+ const char *_fontfile = NULL;
+
+ if (text != NULL)
+ _text = (*env)->GetStringUTFChars(env, text, NULL);
+ if (font != NULL)
+ _font = (*env)->GetStringUTFChars(env, font, NULL);
+ if (fontfile != NULL)
+ _fontfile = (*env)->GetStringUTFChars(env, fontfile, NULL);
+
+ if (vips_text(&out, _text, "font", _font, "width", width, "height", height, "align", align, "justify", justify,
+ "dpi", dpi, "spacing", spacing, "fontfile", _fontfile, "rgba", rgba, NULL))
+ {
+ throwVipsException(env, "Unable to render text image");
+ }
+
+ if (text != NULL)
+ (*env)->ReleaseStringUTFChars(env, text, _text);
+ if (font != NULL)
+ (*env)->ReleaseStringUTFChars(env, font, _font);
+ if (fontfile != NULL)
+ (*env)->ReleaseStringUTFChars(env, fontfile, _fontfile);
+ return (*env)->NewObject(env, cls, ctor_mid, (jlong) out);
+}
+
JNIEXPORT jobject
JNICALL Java_com_criteo_vips_VipsImage_joinNative(JNIEnv *env, jclass cls, jobject in1, jobject in2, jint direction)
diff --git a/src/main/c/VipsImage.h b/src/main/c/VipsImage.h
index a953fa91..5c8ce129 100644
--- a/src/main/c/VipsImage.h
+++ b/src/main/c/VipsImage.h
@@ -359,6 +359,14 @@ JNIEXPORT void JNICALL Java_com_criteo_vips_VipsImage_autorot
JNIEXPORT void JNICALL Java_com_criteo_vips_VipsImage_removeAutorotAngle
(JNIEnv *, jobject);
+/*
+ * Class: com_criteo_vips_VipsImage
+ * Method: textNative
+ * Signature: (Ljava/lang/String;Ljava/lang/String;IIIZIILjava/lang/String;Z)Lcom/criteo/vips/VipsImage;
+ */
+JNIEXPORT jobject JNICALL Java_com_criteo_vips_VipsImage_textNative
+ (JNIEnv *, jclass, jstring, jstring, jint, jint, jint, jboolean, jint, jint, jstring, jboolean);
+
/*
* Class: com_criteo_vips_VipsImage
* Method: clone
diff --git a/src/main/java/com/criteo/vips/Vips.java b/src/main/java/com/criteo/vips/Vips.java
index 0b4786ad..10e374dd 100644
--- a/src/main/java/com/criteo/vips/Vips.java
+++ b/src/main/java/com/criteo/vips/Vips.java
@@ -1,5 +1,5 @@
/*
- Copyright (c) 2019 Criteo
+ Copyright (c) 2022 Criteo
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
@@ -24,6 +24,13 @@ public class Vips {
private static final String SYSTEM_NAME = System.getProperty("os.name").toLowerCase();
private static final String[] LINUX_LIBRARIES = {
+ "freetype",
+ "harfbuzz",
+ "fribidi",
+ "pixman-1",
+ "fontconfig",
+ "cairo",
+ "pango-1.0",
"aom",
"heif",
"exif",
diff --git a/src/main/java/com/criteo/vips/VipsImage.java b/src/main/java/com/criteo/vips/VipsImage.java
index 984a5c88..048a6f42 100644
--- a/src/main/java/com/criteo/vips/VipsImage.java
+++ b/src/main/java/com/criteo/vips/VipsImage.java
@@ -330,6 +330,34 @@ public VipsInterpretation getInterpretation() {
public native void removeAutorotAngle();
+ /**
+ * Draw the string text to an image.
+ * Output image is normally a one-band 8-bit unsigned char image, with 0 for no text and 255 for text.
+ * Values between are used for anti-aliasing.
+ *
+ * @param text is the text to render as a UTF-8 string. It can contain Pango markup, for example TheGuardian
+ * @param font font to render with
+ * @param width image should be no wider than this many pixels
+ * @param height image should be no higher than this many pixels
+ * @param align set justification alignment
+ * @param justify justify lines
+ * @param dpi render at this resolution
+ * @param rgba enable RGBA output
+ * @param spacing space lines by this in points
+ * @param fontfile load this font file
+ * @param rgba enable RGBA output
+ * @throws VipsException if error
+ */
+ public static VipsImage text(String text, String font, int width, int height, VipsAlign align, boolean justify,
+ int dpi, int spacing, String fontfile, boolean rgba)
+ throws VipsException {
+ return textNative(text, font, width, height, align.getValue(), justify, dpi, spacing, fontfile, rgba);
+ }
+
+ private static native VipsImage textNative(String text, String font, int width, int height, int align,
+ boolean justify, int dpi, int spacing, String fontfile, boolean rgba)
+ throws VipsException;
+
public native VipsImage clone() throws VipsException;
public native void release();
diff --git a/src/test/java/com/criteo/vips/VipsImageTest.java b/src/test/java/com/criteo/vips/VipsImageTest.java
index f0f5e1d6..0404a823 100644
--- a/src/test/java/com/criteo/vips/VipsImageTest.java
+++ b/src/test/java/com/criteo/vips/VipsImageTest.java
@@ -970,4 +970,40 @@ public void TestShouldThrowErrorOnOperationIfTruncatedImage() throws IOException
// expected
}
}
+
+ @Test
+ public void TestShouldRenderTextFromDefaultFont() throws VipsException {
+ int expectedBands = 1;
+ int width = 512;
+ int height = 256;
+ int dpi = 144;
+ int spacing = 1;
+ String str = "Hello World!";
+ try (VipsImage text = VipsImage.text(str, null, width, height, VipsAlign.Centre, false, dpi, spacing,
+ null, false)) {
+ assertEquals(expectedBands, text.getBands());
+ }
+ catch (Exception e) {
+ fail("Should not throw exception when rendering text");
+ }
+ }
+
+ @Test
+ public void TestShouldRenderCyrillicTextFromFontfile() throws VipsException {
+ String fontfile = VipsTestUtils.getRessourcePath("Roboto-Regular.ttf");
+ String font = "Roboto Regular 32";
+ int expectedBands = 4;
+ int width = 512;
+ int height = 256;
+ int dpi = 144;
+ int spacing = 1;
+ String str = " Здравейте от София!";
+ try (VipsImage text = VipsImage.text(str, font, width, height, VipsAlign.Centre, false, dpi, spacing,
+ fontfile, true)) {
+ assertEquals(expectedBands, text.getBands());
+ }
+ catch (Exception e) {
+ fail("Should not throw exception when rendering text");
+ }
+ }
}
diff --git a/src/test/resources/Roboto-Regular.ttf b/src/test/resources/Roboto-Regular.ttf
new file mode 100644
index 00000000..3d6861b4
Binary files /dev/null and b/src/test/resources/Roboto-Regular.ttf differ