class

Tryst::Photo

Inherits Reference < Object

A CPU-side RGBA pixel buffer, backed by Tk's "photo" image type.

Despite the name this is really a raw pixel surface, not a picture: "photo" is just Tk's term for a full-color image, as opposed to its "bitmap" type (two colors plus transparency). Think software framebuffer - pack RGBA bytes, write them in bulk, read them back, zoom or subsample, and display the result by handing the image name to a canvas or label. It's all CPU work; nothing here touches a GPU.

The pixel methods go through Tk's C API directly rather than the Tcl-level $photo put, which is fast enough to drive games and real-time visualisation (see Interp#photo_put_block for the difference).

Lifetime

A Tk image is a named, global resource that Tk itself never reclaims. This class registers a finalizer, so the underlying image goes away once the Photo is collected - the same contract as File or Socket: keep the object alive as long as you need the image. If only the name is kept (handed to a widget's image:) and the wrapper is dropped, the image can be reclaimed out from under that widget. Tk shows a broken image rather than crashing, but it doesn't come back; call #delete when you want deterministic cleanup.

photo = Tryst::Photo.new(app, width: 100, height: 100)
photo.put_block(Bytes.new(100 * 100 * 4) { |i| i % 4 == 3 ? 255_u8 : 0_u8 }, 100, 100)
pixel = photo.get_pixel(0, 0) # => {r: 0, g: 0, b: 0, a: 255}

Constructors

from_svg(app : App, path : String | Nil = nil, data : String | Nil = nil, name : String | Nil = nil, dpi : Int32 | Nil = nil, scale : Float64 | Nil = nil, scaletowidth : Int32 | Nil = nil, scaletoheight : Int32 | Nil = nil) : Photo

Loads an SVG file/string via Tk's native -format svg photo image (Tk 9.x; 8.6 has none). No COMPILE-TIME gate - -format svg is a plain Tcl-level image option, not a raw C symbol this binding links against, so whether it works is entirely a property of the Tk LIBRARY actually loaded at runtime, not what TCL_VERSION this binary happened to be compiled with (that only chooses which library gets linked in the first place; see Tryst::TCL_MAJOR_VERSION's own doc comment). But this IS a real RUNTIME gate, checked proactively rather than left to whatever error Tcl happens to raise: app.tcl_major_version asks the actually-loaded interpreter directly (it reads tcl_patchLevel, a real value the interpreter itself reports - not a guess from error text), and this method reads it BEFORE ever touching Tcl, raising a clear, purpose-built error up front rather than relying on Tk's own "image file format "svg" is not supported" message to carry that meaning. This is strictly better than error-text inference, not just clearer: a version check that runs before any Tcl call at all can never misfire on an unrelated failure (a genuinely malformed SVG file still fails on its own, afterward, with Tcl's own real message - the version check only ever answers the one question it's asked). Confirmed directly against both real libraries: 8.6.17 fails the version check immediately; 9.0.3 passes it and loads correctly.

Genuinely useful for a widget's STATIC vector icon asset (an app logo, a fixed decorative glyph) without pulling in tryst-vector at all when the asset never changes at runtime - real path/rect/ circle/ellipse/line/polyline/polygon and linearGradient/ radialGradient support (confirmed via Tk 9.0.3's own photo.ntk manpage), not a toy subset.

Give exactly one of path:/data: (a file path, or inline SVG - the RAW XML TEXT, confirmed directly: unlike Photo.new's own data: for binary formats like PNG, which is base64, SVG's data: is not base64-encoded at all - handing it base64 fails with a generic "couldn't recognize image data", not a helpful one). dpi:/scale:/ scaletowidth:/scaletoheight: control how the vector content rasterizes (dpi: defaults to 96 if omitted); scale:, scaletowidth:, and scaletoheight: are mutually exclusive and each independently aspect-preserving - pick exactly one of the three to control size, never combine two (confirmed directly: Tk rejects any pair of them together, including scaletowidth: with scaletoheight:, with the same generic "couldn't recognize" error rather than naming the conflict - checked here instead, so passing more than one raises a clear ArgumentError up front).

Caveat worth knowing before reaching for this: Tk's SVG renderer silently ignores <text> elements rather than erroring - an asset with text labels loses them with no warning either from Tk or from this method.

Source
new(app : App, name : String | Nil = nil, width : Int32 | Nil = nil, height : Int32 | Nil = nil, file : String | Nil = nil, data : String | Nil = nil, format : String | Nil = nil, palette : String | Nil = nil, gamma : Float64 | Nil = nil)

Create a photo image. width/height give it a fixed size; omit both and it sizes itself to whatever gets written into it (which is what #expand needs - see there). file: loads from a path, data: from base64, format: names the image format (e.g. "png") - and, for formats that take their own sub-options (Tk 9.x's "svg" is the one this codebase cares about), the WHOLE compound string including them (e.g. format: "svg -scaletowidth 40" - confirmed directly: those sub-options are not separate top-level image create options at all, Tk rejects -scaletowidth as "unknown option" if given that way; they only work fused into the -format string itself). Photo.from_svg builds that string for the svg case so a caller doesn't have to know this.

Source

Class methods

delete_task(name : String, app : App) : Proc(Nil)

@api private

The Tcl-side half of what a finalizer needs to do for name/app - split out so #initialize can build it exactly once and stash it in @finalize_task, letting #finalize (see there) enqueue that existing Proc instead of building a fresh one. Confirmed empirically that building new Procs from inside an actual GC finalizer, once more than a handful finalize in the same collection, corrupts Boehm's in-progress finalization batch - other pending finalizers in the same GC.collect silently never ran. Captures name/app as plain locals, not self/@name/@app: closing over self here would keep the Photo permanently reachable from @finalize_task, so it could never be collected in the first place (Ruby's version of this problem; Crystal's GC calls #finalize as a real method on the object itself, so nothing forces the split the way it would in Ruby, but the self-capture trap is the same either way).

Source
finalizer_for(name : String, app : App) : Proc(Nil)

@api private

What #finalize does, as a standalone proc - kept separate purely so a spec can call it directly instead of trying to provoke a real collection, which is genuinely flaky in a shared, long-lived worker process. Not what #finalize itself calls (see .delete_task for why: this allocates a fresh task each call, fine for a spec calling it once, not for an actual finalizer).

A finalizer can run on any thread, so the delete is queued onto the interpreter's own thread (fire-and-forget) rather than going through #tcl_eval, which would block on a cross-thread handoff - not something to do from inside a collection. It goes through #queue_for_main_from_finalizer rather than #queue_for_main for the same reason: #queue_for_main's Channel#send can suspend the calling fiber indefinitely once the channel is full, which a GC finalizer can't risk. Nothing here can raise on the finalizer's own thread - the actual delete happens later, in the proc .delete_task returns, on the main thread. There, the Tcl-level catch covers an ordinary image-delete failure; the surrounding rescue TclError covers the interpreter itself already being torn down by Interp#delete, whose guarded pointer accessor raises a catchable TclError instead of touching freed memory.

Source
next_name

@api private

Source

Instance methods

==(other : Photo) : Bool

Two photos are the same image when they carry the same Tk image name. Comparing against anything else is a compile error - to test a name, say so: photo.name == "img1".

Source
blank

Reset every pixel to fully transparent.

Source
clear

Reset every pixel to fully transparent.

Source
command(*args : TclArgValue, **kwargs : TclArgValue) : String

Run a photo subcommand this class has no dedicated method for - copy, read, write, and so on - with the image name prepended, the same shape as Widget#command.

thumb.command(:copy, source.name, subsample: 4)
Source
delete

Delete the underlying Tk image now, rather than waiting for a collection.

This also disarms the finalizer, so a later collection can't delete an unrelated image that has since taken this name. Ruby does that with ObjectSpace.undefine_finalizer; Crystal has no equivalent (GC.add_finalizer always wires to #finalize, and nothing in the stdlib unregisters it again), so a guard flag stands in. Reaching into Boehm's own GC_register_finalizer_ignore_self would work but ties this to one GC implementation for no observable gain.

Source
exists?

Whether the underlying Tk image is still there.

Source
expand(width : Int32, height : Int32) : self

Grow to at least width x height, never shrinking.

Has no effect at all on a photo created with explicit width:/ height: - Tk only auto-sizes an image whose size came from the pixels written into it. That's Tk's own rule, not this wrapper's.

Source
get_image(x : Int32 = 0, y : Int32 = 0, width : Int32 | Nil = nil, height : Int32 | Nil = nil) : NamedTuple(data: Bytes, width: Int32, height: Int32)

Read pixels back as packed RGBA bytes. width/height default to the rest of the image from (x, y).

There's no unpack: option, unlike ruby-tryst's: it exists there to turn a binary String into an Array of integers, and Bytes is already exactly that - data[0] is the first pixel's red channel.

Source
get_pixel(x : Int32, y : Int32) : NamedTuple(r: Int32, g: Int32, b: Int32, a: Int32)

One pixel's channels, each 0-255.

Source
get_size

Named get_size rather than the size ameba would prefer, to keep the get_size/get_image/get_pixel trio reading as one family - renaming only the argument-less one would split it for no gain.

Source
hash(hasher)

See Object#hash(hasher)

Source
inspect(io : IO) : Nil

Appends a String representation of this object which includes its class name, its object address and the values of all instance variables.

class Person
  def initialize(@name : String, @age : Int32)
  end
end

Person.new("John", 32).inspect # => #<Person:0x10fd31f20 @name="John", @age=32>
Source
name
Source
put_block(pixel_data : Bytes, width : Int32, height : Int32, x : Int32 = 0, y : Int32 = 0, format : PixelFormat = :rgba, composite : PhotoComposite = :set) : self

Write pixel_data - exactly widthheight4 bytes - with its top-left corner at (x, y). See PixelFormat for a non-RGBA source buffer and PhotoComposite for blending rather than overwriting.

Source
put_zoomed_block(pixel_data : Bytes, width : Int32, height : Int32, x : Int32 = 0, y : Int32 = 0, zoom_x : Int32 = 1, zoom_y : Int32 = 1, subsample_x : Int32 = 1, subsample_y : Int32 = 1, format : PixelFormat = :rgba, composite : PhotoComposite = :set) : self

#put_block, scaled as it writes. See Interp#photo_put_zoomed_block.

Source
set_size(width : Int32, height : Int32) : self

Resize, cropping or adding transparent pixels as needed.

Source
to_s(io : IO) : Nil

Appends a short String representation of this object which includes its class name and its object address.

class Person
  def initialize(@name : String, @age : Int32)
  end
end

Person.new("John", 32).to_s # => #<Person:0x10a199f20>
Source