Package limn.graphics

Class Images

java.lang.Object
limn.graphics.Images

public final class Images extends Object
Image loading and saving facade. Loading is backed by the running backend's single ImageDecoder (installed at startup, mirroring TextRulers/Ui); saving is backed by an ordered list of ImageEncoders, because what an encoder can write is chosen by the caller rather than by the input, so more than one can be useful at once.

Both directions are pure CPU (the GPU texture is created lazily at draw time, and encoding touches no GPU at all), so both are safe to call from the UI thread during setup, and safe to call from a worker thread. Encoding in particular needs no backend, no window and no GL context: ImageFormat.PNG works in a headless test.

Pure CPU is not the same as quick: every entry point here reads, decodes or compresses on the thread that calls it, which is a stall if that thread is the UI thread and a frame is due. decodeAsync(byte[]), encodeAsync(limn.graphics.Image, limn.graphics.ImageEncodeOptions), saveAsync(limn.graphics.Image, limn.graphics.ImageEncodeOptions, java.nio.file.Path), loadShared(java.nio.file.Path) and fromResourceShared(java.lang.String) do the same work on the Ui worker pool and hand the result back on the UI thread; unlike their synchronous counterparts, those need a running backend. The one blocking call with no asynchronous form is encode(Image, ImageEncodeOptions, OutputStream), which says on itself why it cannot have one.

Two suffixes, and they are not interchangeable. A name ending in Async returns an unstarted Work: nothing happens until start(), and the job can be cancelled. A name ending in Shared returns a CompletableFuture that is already running and is de-duplicated by source, so two callers asking for one file get one picture and one texture, and neither of them may cancel what the other is also waiting for.

To get an image out of the GPU in the first place, see ReadableSurface (an offscreen surface) and GpuRenderer.captureFramebuffer (a window).

  • Method Details

    • installDecoder

      public static void installDecoder(ImageDecoder newDecoder)
      Installs the backend decoder (called once at backend startup).
    • uninstallDecoder

      public static void uninstallDecoder(ImageDecoder candidate)
      Uninstalls candidate if it is the installed decoder (backend shutdown).
    • isDecoderInstalled

      public static boolean isDecoderInstalled()
      Returns:
      whether a decoder is installed (i.e. a backend is running)
    • decode

      public static Image decode(byte[] fileBytes)
      Decodes an encoded image (PNG/JPG/…) from memory, on the calling thread. Cheap for an icon and not for a photograph, whose decode scales with its pixel count and is a dropped frame when the caller is the UI thread. Use decodeAsync(byte[]) outside setup code.
    • load

      public static Image load(Path file)
      Reads and decodes an image file on the calling thread: a blocking read followed by a decode, so its cost is the disk's as well as the decoder's and is unbounded on a network volume. Use loadShared(java.nio.file.Path) outside setup code.
    • fromResource

      public static Image fromResource(String resource)
      Reads and decodes a classpath resource on the calling thread; see fromResourceShared(java.lang.String) for the form that does it on the worker pool.
    • installEncoder

      public static void installEncoder(ImageEncoder encoder)
      Installs encoder at the end of the probe order. Encoders are asked in the order they were installed, and PngEncoder is already installed when this class loads, so an application that means to replace PNG rather than add a format must uninstallEncoder(limn.graphics.ImageEncoder) PngEncoder.INSTANCE first, or its own encoder is never reached for ImageFormat.PNG. Installing an already installed encoder is a no-op that leaves the order untouched, so running backend startup twice cannot reshuffle priorities.
      Throws:
      NullPointerException - if encoder is null
    • uninstallEncoder

      public static void uninstallEncoder(ImageEncoder encoder)
      Removes encoder. No-op when it was never installed, and a no-op for null rather than a failure: the asymmetry with installEncoder(limn.graphics.ImageEncoder) is deliberate, so a cleanup block can uninstall whatever it may or may not have installed without a null check of its own.
    • uninstallAllEncoders

      public static void uninstallAllEncoders()
      Removes every encoder, the built-in PNG one included. After this, encoding anything fails until something is installed. For a test that wants to observe the empty case; restore the default with installEncoder(PngEncoder.INSTANCE).
    • installedEncoders

      public static List<ImageEncoder> installedEncoders()
      Returns:
      an immutable snapshot of the installed encoders, in the order they are asked
    • canEncode

      public static boolean canEncode(ImageEncodeOptions options)
      Returns:
      whether some installed encoder claims options, the non-throwing form of encode(limn.graphics.Image, limn.graphics.ImageEncodeOptions, java.io.OutputStream), for disabling a menu entry or choosing another format. A true here does not promise that the encode succeeds.
      Throws:
      NullPointerException - if options is null
    • canEncode

      public static boolean canEncode(ImageFormat format)
      Returns:
      whether format can be written at ImageEncodeOptions.DEFAULT_QUALITY.
    • encode

      public static void encode(Image image, ImageEncodeOptions options, OutputStream out) throws IOException
      Encodes image into out with the first installed encoder that claims options. The stream is neither flushed nor closed: the caller owns it.

      If that encoder then fails, the failure propagates and no later encoder is tried: the one that accepted the request is the one that knows what is wrong with it, and replacing that with a generic message would be the worst diagnostic available.

      The whole compress-and-write runs on the calling thread, and there is deliberately no asynchronous form of this overload: out belongs to the caller, and only the caller knows whether writing to it from a worker thread is safe. Wrap this call in Ui.work with a stream you are willing to hand over, or use encodeAsync(limn.graphics.Image, limn.graphics.ImageEncodeOptions) (bytes) or saveAsync(limn.graphics.Image, limn.graphics.ImageEncodeOptions, java.nio.file.Path) (a file), both of which own their sink.

      Throws:
      IOException - if out fails
      UnsupportedOperationException - if no installed encoder claims options; the message names every encoder asked, in order
      NullPointerException - if any argument is null
    • encode

      public static byte[] encode(Image image, ImageEncodeOptions options)
      Encodes image and returns the file bytes. Convenient, and the whole file is in memory; prefer encode(Image, ImageEncodeOptions, OutputStream) or save(Image, ImageEncodeOptions, Path) for anything large.

      Runs the whole compression on the calling thread. At framebuffer sizes that is long enough to be a visible hitch inside a frame, so on the UI thread use encodeAsync(limn.graphics.Image, limn.graphics.ImageEncodeOptions).

      Throws:
      UnsupportedOperationException - if no installed encoder claims options
      UncheckedIOException - never in practice: the sink is a byte array
    • encode

      public static byte[] encode(Image image, ImageFormat format)
      Encodes image as format at ImageEncodeOptions.DEFAULT_QUALITY.
    • save

      public static void save(Image image, ImageEncodeOptions options, Path file)
      Encodes image and writes it to file, replacing whatever was there and creating the parent directories if they are missing. Encode and write both happen on the calling thread; see saveAsync(limn.graphics.Image, limn.graphics.ImageEncodeOptions, java.nio.file.Path) for the form that moves both to the worker pool.
      Throws:
      UnsupportedOperationException - if no installed encoder claims options
      UncheckedIOException - if the file cannot be written
    • save

      public static void save(Image image, ImageFormat format, Path file)
      Saves image as format at ImageEncodeOptions.DEFAULT_QUALITY.

      The format is a parameter and is never inferred from the file name. A suffix is a hint that anyone can get wrong, and inferring from it turns a mistyped name into a file whose contents disagree with its extension, which nothing downstream can detect.

    • saveAsync

      public static Work<Path> saveAsync(Image image, ImageEncodeOptions options, Path file)
      Encodes and writes file on the Ui worker pool and hands file back to onSuccess on the UI thread. Requires a running backend, unlike the synchronous form.

      Returned unstarted, like everything else here whose name ends in Async: attach the handlers, then start(). Dropping it writes nothing at all.

      This is where the work belongs when the image came from a readback: the read itself is a GPU operation and cannot leave the UI thread, but the encode that follows it is plain CPU work on a finished Image, and at framebuffer sizes it is long enough to be a visible hitch if it runs inside a frame.

      The result is a path with nothing to release, so no onDiscarded is attached; a cancelled save may still have written the file, because cancelling stops the delivery and not the body.

      Throws:
      NullPointerException - if any argument is null
      IllegalStateException - if no backend is running
    • encodeAsync

      public static Work<byte[]> encodeAsync(Image image, ImageEncodeOptions options)
      Compresses image on the Ui worker pool and hands the file bytes to onSuccess on the UI thread, the in-memory counterpart of saveAsync(limn.graphics.Image, limn.graphics.ImageEncodeOptions, java.nio.file.Path), for the caller who wants the bytes rather than a file: an upload, a clipboard payload, a diff against a reference. Requires a running backend, unlike the synchronous encode(limn.graphics.Image, limn.graphics.ImageEncodeOptions, java.io.OutputStream).

      Returned unstarted, so the caller can attach handlers first:

      
       Images.encodeAsync(shot, new ImageEncodeOptions(ImageFormat.PNG))
             .onSuccess(bytes -> clipboard.set(bytes))
             .onFailure(error -> status.setText(error.getMessage()))
             .deliverIf(this::isAttached)
             .start();
       

      Every failure arrives at onFailure on the UI thread, including "no installed encoder claims these options": the encoder is chosen when the body runs, not when this returns, so a format nothing can write is a delivered failure rather than a throw at the call site. The result is a plain array with nothing to release, so no onDiscarded is attached and a result nobody takes is simply garbage.

      No progress is reported: an encoder is one call with no interior to report from. And cancelling after the body has begun does not stop the compression (it only prevents the delivery) because the pool is never interrupted.

      Parameters:
      image - the picture to compress; read but not retained
      options - format and quality, as for encode(Image, ImageEncodeOptions)
      Throws:
      NullPointerException - if any argument is null
      IllegalStateException - if no backend is running
    • loadShared

      public static CompletableFuture<Image> loadShared(Path file)
      Reads and decodes file on the Ui worker pool; the returned future is already running and completes on the UI thread, so loadShared(file).thenAccept(view::setImage) sets the image where widget mutation is legal. Requires a running backend (a Ui runtime and an installed decoder), unlike the synchronous load(java.nio.file.Path).

      Shared, which is why this is not a Work and not called loadAsync. Loads are de-duplicated by absolute path: concurrent and later calls for the same path return the same future and therefore the same Image instance, which is what makes one texture serve every user of that file; the backend's texture cache keys by instance identity, so decoding the same file twice would upload it twice. A result with two consumers cannot also be a job either of them may cancel, so there is nothing to start and nothing to withdraw here. A failed load is evicted, so a later call retries rather than replaying the failure forever, and clearSharedCache() forces a re-read after the file changed on disk.

      The failure reaches observers through the future, on the UI thread (exceptionally, whenComplete); nothing is thrown from this method beyond an argument check.

    • fromResourceShared

      public static CompletableFuture<Image> fromResourceShared(String resource)
      Reads and decodes a classpath resource on the Ui worker pool; the returned future is already running and completes on the UI thread. De-duplicated by resource name, retried after a failure and failing through the future exactly as loadShared(java.nio.file.Path) describes, and likewise requiring a running backend.
    • decodeAsync

      public static Work<Image> decodeAsync(byte[] fileBytes)
      Decodes in-memory bytes on the Ui worker pool and hands the picture to onSuccess on the UI thread, carrying the decoder's failure to onFailure when it throws. Requires a running backend.

      Returned unstarted: attach the handlers, then start().

      Uncached, unlike loadShared(java.nio.file.Path): bytes have no stable name to de-duplicate by, so two calls with the same array decode twice and produce two Image instances, and so two GPU textures. Hold the result rather than re-decoding. Being unshared is exactly what lets this one be a cancellable job.

      The caller keeps ownership of fileBytes and must not modify the array until the job has delivered: the decode reads it from a worker thread.

      Throws:
      IllegalStateException - if no backend is running
    • clearSharedCache

      public static void clearSharedCache()
      Drops every shared load, so the next loadShared(java.nio.file.Path) or fromResourceShared(java.lang.String) of a source reads it again (e.g. after files changed on disk). Image instances already handed out are unaffected and keep their textures; only the mapping from source to future is cleared. Any thread.