Class Images
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 Summary
Modifier and TypeMethodDescriptionstatic booleancanEncode(ImageEncodeOptions options) static booleancanEncode(ImageFormat format) static voidDrops every shared load, so the nextloadShared(java.nio.file.Path)orfromResourceShared(java.lang.String)of a source reads it again (e.g.static Imagedecode(byte[] fileBytes) Decodes an encoded image (PNG/JPG/…) from memory, on the calling thread.decodeAsync(byte[] fileBytes) Decodes in-memory bytes on theUiworker pool and hands the picture toonSuccesson the UI thread, carrying the decoder's failure toonFailurewhen it throws.static byte[]encode(Image image, ImageEncodeOptions options) Encodesimageand returns the file bytes.static voidencode(Image image, ImageEncodeOptions options, OutputStream out) Encodesimageintooutwith the first installed encoder that claimsoptions.static byte[]encode(Image image, ImageFormat format) static Work<byte[]> encodeAsync(Image image, ImageEncodeOptions options) Compressesimageon theUiworker pool and hands the file bytes toonSuccesson the UI thread, the in-memory counterpart ofsaveAsync(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.static ImagefromResource(String resource) Reads and decodes a classpath resource on the calling thread; seefromResourceShared(java.lang.String)for the form that does it on the worker pool.static CompletableFuture<Image> fromResourceShared(String resource) Reads and decodes a classpath resource on theUiworker pool; the returned future is already running and completes on the UI thread.static voidinstallDecoder(ImageDecoder newDecoder) Installs the backend decoder (called once at backend startup).static List<ImageEncoder> static voidinstallEncoder(ImageEncoder encoder) Installsencoderat the end of the probe order.static booleanstatic ImageReads 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.static CompletableFuture<Image> loadShared(Path file) Reads and decodesfileon theUiworker pool; the returned future is already running and completes on the UI thread, soloadShared(file).thenAccept(view::setImage)sets the image where widget mutation is legal.static voidsave(Image image, ImageEncodeOptions options, Path file) Encodesimageand writes it tofile, replacing whatever was there and creating the parent directories if they are missing.static voidsave(Image image, ImageFormat format, Path file) saveAsync(Image image, ImageEncodeOptions options, Path file) Encodes and writesfileon theUiworker pool and handsfileback toonSuccesson the UI thread.static voidRemoves every encoder, the built-in PNG one included.static voiduninstallDecoder(ImageDecoder candidate) Uninstallscandidateif it is the installed decoder (backend shutdown).static voiduninstallEncoder(ImageEncoder encoder) Removesencoder.
-
Method Details
-
installDecoder
Installs the backend decoder (called once at backend startup). -
uninstallDecoder
Uninstallscandidateif 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
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. UsedecodeAsync(byte[])outside setup code. -
load
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. UseloadShared(java.nio.file.Path)outside setup code. -
fromResource
Reads and decodes a classpath resource on the calling thread; seefromResourceShared(java.lang.String)for the form that does it on the worker pool. -
installEncoder
Installsencoderat the end of the probe order. Encoders are asked in the order they were installed, andPngEncoderis already installed when this class loads, so an application that means to replace PNG rather than add a format mustuninstallEncoder(limn.graphics.ImageEncoder)PngEncoder.INSTANCEfirst, or its own encoder is never reached forImageFormat.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- ifencoderis null
-
uninstallEncoder
Removesencoder. No-op when it was never installed, and a no-op for null rather than a failure: the asymmetry withinstallEncoder(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 withinstallEncoder(PngEncoder.INSTANCE). -
installedEncoders
- Returns:
- an immutable snapshot of the installed encoders, in the order they are asked
-
canEncode
- Returns:
- whether some installed encoder claims
options, the non-throwing form ofencode(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- ifoptionsis null
-
canEncode
- Returns:
- whether
formatcan be written atImageEncodeOptions.DEFAULT_QUALITY.
-
encode
public static void encode(Image image, ImageEncodeOptions options, OutputStream out) throws IOException Encodesimageintooutwith the first installed encoder that claimsoptions. 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:
outbelongs to the caller, and only the caller knows whether writing to it from a worker thread is safe. Wrap this call inUi.workwith a stream you are willing to hand over, or useencodeAsync(limn.graphics.Image, limn.graphics.ImageEncodeOptions)(bytes) orsaveAsync(limn.graphics.Image, limn.graphics.ImageEncodeOptions, java.nio.file.Path)(a file), both of which own their sink.- Throws:
IOException- ifoutfailsUnsupportedOperationException- if no installed encoder claimsoptions; the message names every encoder asked, in orderNullPointerException- if any argument is null
-
encode
Encodesimageand returns the file bytes. Convenient, and the whole file is in memory; preferencode(Image, ImageEncodeOptions, OutputStream)orsave(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 claimsoptionsUncheckedIOException- never in practice: the sink is a byte array
-
encode
-
save
Encodesimageand writes it tofile, replacing whatever was there and creating the parent directories if they are missing. Encode and write both happen on the calling thread; seesaveAsync(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 claimsoptionsUncheckedIOException- if the file cannot be written
-
save
SavesimageasformatatImageEncodeOptions.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
Encodes and writesfileon theUiworker pool and handsfileback toonSuccesson 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, thenstart(). 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 finishedImage, 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
onDiscardedis 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 nullIllegalStateException- if no backend is running
-
encodeAsync
Compressesimageon theUiworker pool and hands the file bytes toonSuccesson the UI thread, the in-memory counterpart ofsaveAsync(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 synchronousencode(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
onFailureon 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 noonDiscardedis 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 retainedoptions- format and quality, as forencode(Image, ImageEncodeOptions)- Throws:
NullPointerException- if any argument is nullIllegalStateException- if no backend is running
-
decodeAsync
Decodes in-memory bytes on theUiworker pool and hands the picture toonSuccesson the UI thread, carrying the decoder's failure toonFailurewhen 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 twoImageinstances, 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
fileBytesand 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
-