0

I am currently writing a program, which batch processes different images. So I thought that it might be clever to do the operation (Scaling / adding watermark) in parallel.

The problem is I get the following error:

    Exception in thread "main" java.util.concurrent.ExecutionException: java.lang.NullPointerException
    at java.base/java.util.concurrent.ForkJoinTask.get(ForkJoinTask.java:1006)
    at eu.reisihub.soft.watermarking.Main$main$2$2$2$2.invoke(Main.kt:62)
    at eu.reisihub.soft.watermarking.Main$main$2$2$2$2.invoke(Main.kt:19)
    at kotlin.sequences.TransformingSequence$iterator$1.next(Sequences.kt:149)
    at kotlin.sequences.TransformingSequence$iterator$1.next(Sequences.kt:149)
    at kotlin.sequences.SequencesKt___SequencesKt.count(_Sequences.kt:1006)
    at eu.reisihub.soft.watermarking.Main.main(Main.kt:66)
Caused by: java.lang.NullPointerException
    at java.base/jdk.internal.reflect.NativeConstructorAccessorImpl.newInstance0(Native Method)
    at java.base/jdk.internal.reflect.NativeConstructorAccessorImpl.newInstance(NativeConstructorAccessorImpl.java:62)
    at java.base/jdk.internal.reflect.DelegatingConstructorAccessorImpl.newInstance(DelegatingConstructorAccessorImpl.java:45)
    at java.base/java.lang.reflect.Constructor.newInstance(Constructor.java:488)
    at java.base/java.util.concurrent.ForkJoinTask.getThrowableException(ForkJoinTask.java:603)
    ... 7 more
Caused by: java.lang.NullPointerException
    at java.desktop/java.awt.color.ICC_Profile.intFromBigEndian(ICC_Profile.java:1784)
    at java.desktop/java.awt.color.ICC_Profile.getNumComponents(ICC_Profile.java:1476)
    at java.desktop/sun.java2d.cmm.lcms.LCMSTransform.<init>(LCMSTransform.java:93)
    at java.desktop/sun.java2d.cmm.lcms.LCMS.createTransform(LCMS.java:173)
    at java.desktop/java.awt.color.ICC_ColorSpace.fromRGB(ICC_ColorSpace.java:230)
    at java.desktop/com.sun.imageio.plugins.jpeg.JPEGImageReader.setImageData(JPEGImageReader.java:808)
    at java.desktop/com.sun.imageio.plugins.jpeg.JPEGImageReader.readImageHeader(Native Method)
    at java.desktop/com.sun.imageio.plugins.jpeg.JPEGImageReader.readNativeHeader(JPEGImageReader.java:723)
    at java.desktop/com.sun.imageio.plugins.jpeg.JPEGImageReader.checkTablesOnly(JPEGImageReader.java:347)
    at java.desktop/com.sun.imageio.plugins.jpeg.JPEGImageReader.gotoImage(JPEGImageReader.java:493)
    at java.desktop/com.sun.imageio.plugins.jpeg.JPEGImageReader.readHeader(JPEGImageReader.java:716)
    at java.desktop/com.sun.imageio.plugins.jpeg.JPEGImageReader.readInternal(JPEGImageReader.java:1173)
    at java.desktop/com.sun.imageio.plugins.jpeg.JPEGImageReader.read(JPEGImageReader.java:1153)
    at java.desktop/javax.imageio.ImageIO.read(ImageIO.java:1468)
    at java.desktop/javax.imageio.ImageIO.read(ImageIO.java:1363)
    at eu.reisihub.shot.UtilsKt.readImage(Utils.kt:19)
    at eu.reisihub.soft.watermarking.WatermarkUtils$create$1.invoke(WatermarkUtils.kt:18)
    at eu.reisihub.soft.watermarking.WatermarkUtils$create$1.invoke(WatermarkUtils.kt:8)
    at eu.reisihub.soft.watermarking.Main$sam$java_util_concurrent_Callable$0.call(Main.kt)
    at java.base/java.util.concurrent.ForkJoinTask$AdaptedCallable.exec(ForkJoinTask.java:1448)
    at java.base/java.util.concurrent.ForkJoinTask.doExec(ForkJoinTask.java:290)
    at java.base/java.util.concurrent.ForkJoinPool.runWorker(ForkJoinPool.java:1603)
    at java.base/java.util.concurrent.ForkJoinWorkerThread.run(ForkJoinWorkerThread.java:177)

please note, that I know that there is a NPE in java.desktop/java.awt.color.ICC_Profile.intFromBigEndian(ICC_Profile.java:1784). And believe me, it is NOT the image.

I tried 2 approaches with Java 8 and Java 10. I have a fixed size thread pool with 6 threads for 4 cores. I am using Kotlin for the implementation and created an extension function fun Path.readImage(): BufferedImage. For people, who don't know Kotlin, for the purpose of this example only, would correspond roughly to public BufferedImage readImage(Path this).

Naive approach Reading the file using Files.newInputStream(this, StandardOpenOption.READ).use { ImageIO.read(it) }. This opens a new InputStream from the Path and tells ImageIO to read the image from this InputStream. Looks fine? Does not work ~every fourth time on Java 8 and roughly 90% on Java 10. BTW: If I wrap a synchronized block around this, it works - always. With 13 seconds to treat 17 images instead of 7 seconds.

In the meantime I updated Gradle from 4.4 to 4.7. Before I configured IntelliJ to use Java 10 and Gradle to use Java 8. Might be the reason for the differences highlighted above. Side note. I always used synchronized(System.err)!

Sophisticated approach Knowing it works when synchronizing everything my method now looks more complicated and like this:

fun Path.readImage(): BufferedImage = Files.newInputStream(this, StandardOpenOption.READ).buffered().use {
    var nStream: ImageInputStream? = null
    var nReader: ImageReader? = null
    synchronized(System.err) {
        nStream = ImageIO.createImageInputStream(it) ?: throw IIOException("Can't create an ImageInputStream!")

        val iter = ImageIO.getImageReaders(nStream)
        if (!iter.hasNext()) {
            throw IIOException("No image nReader found!")
        }
        nReader = iter.next()
    }

    nStream!!.let { stream ->
        nReader!!.let { reader ->
            reader.setInput(stream, true, true)
            try {
                println(reader)
                return reader.read(0, null)
            } finally {
                reader.dispose()
                stream.close()
            }
        }
    }
}

See the println in the code? I know that for every Image a new com.sun.imageio.plugins.jpeg.JPEGImageReaderobject is created.This improves the performance to the 7 seconds I was talking to. Usually does not work 2-4 times and then works 6-10 times.

The stacktrace for the sophisticated version is the following:

Exception in thread "main" java.util.concurrent.ExecutionException: java.lang.NullPointerException
    at java.util.concurrent.FutureTask.report(FutureTask.java:122)
    at java.util.concurrent.FutureTask.get(FutureTask.java:192)
    at eu.reisihub.soft.watermarking.Main$main$1$2$2$2$2.invoke(Main.kt:64)
    at eu.reisihub.soft.watermarking.Main$main$1$2$2$2$2.invoke(Main.kt:20)
    at kotlin.sequences.TransformingSequence$iterator$1.next(Sequences.kt:149)
    at kotlin.sequences.TransformingSequence$iterator$1.next(Sequences.kt:149)
    at kotlin.sequences.SequencesKt___SequencesKt.count(_Sequences.kt:1006)
    at eu.reisihub.soft.watermarking.Main$main$1.invoke(Main.kt:68)
    at eu.reisihub.soft.watermarking.Main$main$1.invoke(Main.kt:20)
    at eu.reisihub.shot.UtilsKt.measured(Utils.kt:54)
    at eu.reisihub.soft.watermarking.Main.main(Main.kt:24)
Caused by: java.lang.NullPointerException
    at java.awt.color.ICC_Profile.intFromBigEndian(ICC_Profile.java:1782)
    at java.awt.color.ICC_Profile.getNumComponents(ICC_Profile.java:1474)
    at sun.java2d.cmm.lcms.LCMSTransform.<init>(LCMSTransform.java:98)
    at sun.java2d.cmm.lcms.LCMS.createTransform(LCMS.java:173)
    at java.awt.color.ICC_ColorSpace.fromRGB(ICC_ColorSpace.java:218)
    at com.sun.imageio.plugins.jpeg.JPEGImageReader.setImageData(JPEGImageReader.java:694)
    at com.sun.imageio.plugins.jpeg.JPEGImageReader.readImageHeader(Native Method)
    at com.sun.imageio.plugins.jpeg.JPEGImageReader.readNativeHeader(JPEGImageReader.java:609)
    at com.sun.imageio.plugins.jpeg.JPEGImageReader.checkTablesOnly(JPEGImageReader.java:347)
    at com.sun.imageio.plugins.jpeg.JPEGImageReader.gotoImage(JPEGImageReader.java:481)
    at com.sun.imageio.plugins.jpeg.JPEGImageReader.readHeader(JPEGImageReader.java:602)
    at com.sun.imageio.plugins.jpeg.JPEGImageReader.readInternal(JPEGImageReader.java:1059)
    at com.sun.imageio.plugins.jpeg.JPEGImageReader.read(JPEGImageReader.java:1039)
    at eu.reisihub.shot.UtilsKt.readImage(Utils.kt:39)
    at eu.reisihub.soft.watermarking.WatermarkUtils$create$1.invoke(WatermarkUtils.kt:16)
    at eu.reisihub.soft.watermarking.WatermarkUtils$create$1.invoke(WatermarkUtils.kt:8)
    at eu.reisihub.soft.watermarking.Main$sam$java_util_concurrent_Callable$0.call(Main.kt)
    at java.util.concurrent.FutureTask.run(FutureTask.java:266)
    at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1142)
    at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:617)
    at java.lang.Thread.run(Thread.java:748)

Where Main:64 is Future#get And Main:68 is kotlin.Sequence#count

Location the Main.kt file: https://github.com/reisi007/Reisishot-Photo-Tools/blob/ea32a605657226edb6140911fd02f77d006163d3/Watermarking/src/main/kotlin/eu/reisihub/soft/watermarking/Main.kt

Location of Utils.kt file: https://github.com/reisi007/Reisishot-Photo-Tools/blob/ea32a605657226edb6140911fd02f77d006163d3/base/src/main/kotlin/eu/reisihub/shot/Utils.kt

I have tried improving the code for several hours and my head stopped spawning new ideas by now. I know that the mistake also happens with 2 pictures. As it does not always fail, the ICC profile must be correct. I am - honestly - conidering that my HDD is not sound and occasionally messes up. I also tried reading the files from my SSD. As per https://stackoverflow.com/a/26300361/1870799 the read method should be thread-safe. I have no idea whats happening. Every image on disk (represented by a Path), gets it's own Callable<Task>. I am only reading those images and writing it to a different folder. So foreach task one file is read, one file is written. These two files are NOT the same, every task has its own files.

The only thing they share is a BufferedImage in PNG format, which is drawn on the read image (watermark). But that happens later. As can be seen in the stacktrace it happens when reading a JPEG image.

at com.sun.imageio.plugins.jpeg.JPEGImageReader.read(JPEGImageReader.java:1039)
at eu.reisihub.shot.UtilsKt.readImage(Utils.kt:39)

If you want to execute the program, the JSON setting, whose path the main app needs, can be created using https://github.com/reisi007/Reisishot-Photo-Tools/blob/ea32a605657226edb6140911fd02f77d006163d3/Watermarking/src/main/kotlin/eu/reisihub/soft/watermarking/SettingsCreator.kt

I appreciate any input. At the current point I do not know what is wrong...

BTW: For some tasks I am using: https://github.com/coobird/thumbnailator

Florian Reisinger
  • 2,638
  • 4
  • 23
  • 34
  • What's your current code? – Mạnh Quyết Nguyễn May 22 '18 at 06:42
  • I linked the current code: https://github.com/reisi007/Reisishot-Photo-Tools/tree/master/Watermarking/src/main/kotlin/eu/reisihub/soft/watermarking and https://github.com/reisi007/Reisishot-Photo-Tools/blob/master/base/src/main/kotlin/eu/reisihub/shot/Utils.kt It is available at Github – Florian Reisinger May 22 '18 at 06:46
  • 1
    It needs to be available *here*. Nobody is going to chase your link. – user207421 May 22 '18 at 07:11
  • The *relevant* pieces of code in facr are available. `Files.newInputStream(this, StandardOpenOption.READ).use { ImageIO.read(it) }` This [when called from multiple threads] is the place where the exception happens. It won't happen if this piece is `synchronized`. Context: From a list of all JPG files, create task which reads one file and the error should be visible. – Florian Reisinger May 22 '18 at 13:39
  • Just a hunch, but I think that putting `ICC_Profile.getInstance(ColorSpace.CS_sRGB).getData();` in a `static` initializer block in your `UtilsKt` class will fix the problem. See [this](https://github.com/haraldk/TwelveMonkeys/issues/402) for further info. ;-) – Harald K May 22 '18 at 16:44
  • @haraldK: This workaround works fine! – Florian Reisinger May 23 '18 at 04:48

2 Answers2

3

UPDATE 2018-05-23

@haraldK mentioned in a comment that loading images profiles before Code:

import java.awt.color.ColorSpace
import java.awt.color.ICC_Profile
import java.awt.image.BufferedImage
import java.nio.file.Path
import javax.imageio.ImageIO

object ImgLoadUtils {
    init {
        // Load deferred color space profiles to avoid 
        // ConcurrentModificationException due to JDK
        // Use in public static main void or prior to application initialization
        // https://github.com/haraldk/TwelveMonkeys/issues/402
        // https://bugs.openjdk.java.net/browse/JDK-6986863
        // https://stackoverflow.com/questions/26297491/imageio-thread-safety
        ICC_Profile.getInstance(ColorSpace.CS_sRGB).getData();
        ICC_Profile.getInstance(ColorSpace.CS_PYCC).getData();
        ICC_Profile.getInstance(ColorSpace.CS_GRAY).getData();
        ICC_Profile.getInstance(ColorSpace.CS_CIEXYZ).getData();
        ICC_Profile.getInstance(ColorSpace.CS_LINEAR_RGB).getData();
    }

    fun loadImage(p: Path): BufferedImage = ImageIO.read(p.toFile())
}

SOURCE


Thanks for @Alex Taylor for the OpenJDK bug link. That assured me that I am not a total idiot.

Instead of

fun Path.readImage(): BufferedImage =
    Files.newInputStream(this, StandardOpenOption.READ).use { ImageIO.read(it) }

I am now using

fun Path.readImage(): BufferedImage =
    ImageIcon(toUri().toURL()).let {
        BufferedImage(it.iconWidth, it.iconHeight, BufferedImage.TYPE_INT_ARGB).apply {
            it.paintIcon(null, createGraphics(), 0, 0)
        }
    }

This is per https://aacsinia.wordpress.com/2010/12/21/java-how-to-create-buffered-image-from-inputstream/. Speed is on par and error is gone [20+ runs]! IMHO ImageIO.read therefore should not be used in a multithreaded environment....

Florian Reisinger
  • 2,638
  • 4
  • 23
  • 34
2

There is a bug report for OpenJDK titled 'NullPointerException from ICC_Profile.getInstance() in multi-thread application'. There is no resolution at time of writing.

Alex Taylor
  • 8,343
  • 4
  • 25
  • 40