Tryst::Photo
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
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.
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.
Class methods
@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).
@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.
Instance methods
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".
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)
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.
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.
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.
One pixel's channels, each 0-255.
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.
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>
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.
#put_block, scaled as it writes. See Interp#photo_put_zoomed_block.
Resize, cropping or adding transparent pixels as needed.