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).
-
Field Summary
FieldsModifier and TypeFieldDescriptionstatic final longThe bytes of frames an animation may have for all of them to be decoded when it is loaded, unlesssetAnimationBudget(long)says otherwise: 32 MiB, some hundred frames of 320 by 240.static final longThe bytes of image textures a window keeps unlesssetTextureBudget(long)says otherwise: 256 MiB, counted as the GPU holds them, four bytes a pixel and about a third again for the smaller copies a picture is sampled from when it is drawn below its size. -
Method Summary
Modifier and TypeMethodDescriptionstatic ImageMakes an animated image of frames from anywhere.static longstatic 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/GIF/WebP/…) 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 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> fromResourcePinned(String resource) Loads a classpath resource asfromResourceShared(java.lang.String)does and keeps the result whatever the budget, until as manyunpinResource(java.lang.String)s as calls; seeloadPinned(java.nio.file.Path).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 ImageMakes an animated image of a sheet of frames laid out in a grid, read along each row from the top: the frames of a sprite, drawn side by side in one picture.static ImageAsfromSheet(Image, int, int, int), for a sheet whose last row is not full.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> loadPinned(Path file) LoadsfileasloadShared(java.nio.file.Path)does and keeps the result whatever the budget: it is never let go and counts nothing toward it, untilunpin(Path).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 voidsetAnimationBudget(long bytes) Moves the size up to which an animated picture is decoded whole.static voidsetSharedBudget(long bytes) Moves the budget the shared loads are kept within.static voidsetTextureBudget(long bytes) Moves the budget each window keeps its image textures within, from that window's next frame on.static longThe budget the shared loads are kept within; seesetSharedBudget(long).static longThe budget each window keeps its image textures within:DEFAULT_TEXTURE_BUDGETuntilsetTextureBudget(long)moves it.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.static voidTakes back oneloadPinned(java.nio.file.Path)of a file; when every one is taken back, the file is back among the shared loads the budget keeps or lets go.static voidunpinResource(String resource) Takes back onefromResourcePinned(java.lang.String)of a resource; when every one is taken back, the resource is back among the shared loads the budget keeps or lets go.
-
Field Details
-
DEFAULT_ANIMATION_BUDGET
public static final long DEFAULT_ANIMATION_BUDGETThe bytes of frames an animation may have for all of them to be decoded when it is loaded, unlesssetAnimationBudget(long)says otherwise: 32 MiB, some hundred frames of 320 by 240.- See Also:
-
DEFAULT_TEXTURE_BUDGET
public static final long DEFAULT_TEXTURE_BUDGETThe bytes of image textures a window keeps unlesssetTextureBudget(long)says otherwise: 256 MiB, counted as the GPU holds them, four bytes a pixel and about a third again for the smaller copies a picture is sampled from when it is drawn below its size. That is some twenty-four pictures of 1920 × 1080. Three quarters of it sits on the heap as well, because a cached texture keeps itsImageand the image keeps its pixels.- See Also:
-
-
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/GIF/WebP/…) from memory, on the calling thread. An animated GIF, PNG or WebP is an animated image, every frame of it. 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.An SVG is drawn at its own size, one pixel to one of its units, by the installed SVG renderer, and shows only the pictures it embeds: read from a file through
SvgIcon.load(java.nio.file.Path), it shows those beside it too, and anSvgIconis what draws one sharp at any size. -
setAnimationBudget
public static void setAnimationBudget(long bytes) Moves the size up to which an animated picture is decoded whole. Under it every frame is in memory from the start, and playing costs nothing; over it the picture keeps its file's bytes and decodes a few frames ahead of the one shown, on the worker pool, as it plays, so that a long animation costs its file and a handful of frames and not every frame at once. A frame that is not ready when its time comes is not waited for: the one before it stays. It applies to pictures decoded from now on. Any thread.- Parameters:
bytes- the most bytes of frames, four a pixel, decoded at once; 0 decodes every animation as it plays- Throws:
IllegalArgumentException- ifbytesis negative
-
animationBudget
public static long animationBudget()- Returns:
- the size up to which an animated picture is decoded whole; see
setAnimationBudget(long)
-
animation
Makes an animated image of frames from anywhere. Its own pixels are the first frame's, so whatever draws it without playing it draws that frame.- Parameters:
frames- the frames in order, each of the first one's size; at least onemillis- how long each is shown, in milliseconds, one for each frame, each above 0loopCount- how many turns it asks for, 0 for one that does not end- Returns:
- the image; with one frame, a still image, that frame
- Throws:
IllegalArgumentException- if there is no frame, the sizes differ, a time is not above 0, the times are not one a frame, or a frame is itself animated
-
fromSheet
Makes an animated image of a sheet of frames laid out in a grid, read along each row from the top: the frames of a sprite, drawn side by side in one picture. Each frame becomes an image of its own, and the animation turns without end.- Parameters:
sheet- the picture holding the framescolumns- how many frames acrossrows- how many downframeMillis- how long each frame is shown, in milliseconds- Returns:
- the animation
- Throws:
IllegalArgumentException- if the grid does not divide the sheet evenly, or a number is not above 0
-
fromSheet
AsfromSheet(Image, int, int, int), for a sheet whose last row is not full.- Parameters:
frames- how many frames the sheet holds, from 1 tocolumns * rows
-
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)andinstallEncoder(GifEncoder.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 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 -> upload.send(bytes)) .onFailure(error -> status.setText(error.getMessage())) .deliverIf(this::isShowing) .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
-
loadPinned
LoadsfileasloadShared(java.nio.file.Path)does and keeps the result whatever the budget: it is never let go and counts nothing toward it, untilunpin(Path). For a picture an application wants at hand for as long as it runs, at the price of the memory it takes for that long. A path already loaded and shared is pinned from this call on, and is the same object.Pins are counted: each call is taken back by one
unpin(Path), so two parts of an application that each pin a file keep it pinned until both have unpinned it.- Parameters:
file- the file to read- Returns:
- the shared future, already running or already complete, completing on the UI thread
-
unpin
Takes back oneloadPinned(java.nio.file.Path)of a file; when every one is taken back, the file is back among the shared loads the budget keeps or lets go. Nothing happens for a file that is not pinned. Any thread.- Parameters:
file- the file a load was pinned by
-
fromResourcePinned
Loads a classpath resource asfromResourceShared(java.lang.String)does and keeps the result whatever the budget, until as manyunpinResource(java.lang.String)s as calls; seeloadPinned(java.nio.file.Path).- Parameters:
resource- the resource's name- Returns:
- the shared future, already running or already complete, completing on the UI thread
-
unpinResource
Takes back onefromResourcePinned(java.lang.String)of a resource; when every one is taken back, the resource is back among the shared loads the budget keeps or lets go. Nothing happens for a resource that is not pinned. Any thread.- Parameters:
resource- the name a load was pinned by
-
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
-
setTextureBudget
public static void setTextureBudget(long bytes) Moves the budget each window keeps its image textures within, from that window's next frame on.A texture is made when an image is first drawn, and each window caches its own. A window drops one at a frame boundary once 300 frames have gone by without drawing it, and sooner, least recently drawn first, while what it holds is over this budget. A texture the frame just finished drew is never dropped, so the budget is soft: a single picture larger than it still shows, and stays for as long as every frame draws it, and a screen whose pictures together exceed it keeps them all while each frame draws them all. A frame that repaints only part of a window draws only the pictures in that part, so over the budget a picture elsewhere can be dropped while it is still on screen. Drawing an image whose texture was dropped makes it again, at the cost of an upload in that frame.
Raise it for an application that keeps coming back to a set of large pictures bigger than the default, at the cost in memory the default describes; lower it where memory is short. The budget is per window, so two windows full of pictures may hold twice it. A window that is not drawing renders no frames and drops nothing until it draws again. Any thread.
- Parameters:
bytes- the most texture bytes, mipmaps included, a window keeps unless its last frame drew more; 0 keeps only what that frame drew- Throws:
IllegalArgumentException- ifbytesis negative
-
textureBudget
public static long textureBudget()The budget each window keeps its image textures within:DEFAULT_TEXTURE_BUDGETuntilsetTextureBudget(long)moves it.- Returns:
- the current budget, in bytes
-