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