Class: Teek::Photo

Inherits:
Object
  • Object
show all
Defined in:
lib/teek/photo.rb

Overview

CPU-side RGBA pixel buffer backed by Tk's "photo image" format.

Despite the name, this is really a raw pixel manipulation surface. Tk has two built-in image types: "bitmap" (two colors + transparency) and "photo" (full-color, 32-bit RGBA). The naming reflects Tk's image type system, not the contents — a "photo" is just Tk's term for a full-color pixel buffer.

Think of it as a software framebuffer: you pack RGBA bytes, write them in bulk, read them back, zoom/subsample, and blit to a canvas or label for display. All work is CPU-driven — there is no GPU acceleration.

The C methods (#put_block, #get_image, #get_pixel, etc.) call Tk_PhotoPutBlock / Tk_PhotoGetImage directly, bypassing Tcl string parsing for much better performance than the Tcl-level $photo put command. Designed for games, visualizations, and real-time drawing.

Examples:

Create and fill with red pixels

photo = Teek::Photo.new(app, width: 100, height: 100)
red = ([255, 0, 0, 255].pack('CCCC')) * (100 * 100)
photo.put_block(red, 100, 100)

Read pixels back

result = photo.get_image
r, g, b, a = result[:data][0, 4].unpack('CCCC')

Zoom a small sprite

sprite = Teek::Photo.new(app, width: 64, height: 64)
# ... fill sprite pixels ...
dest = Teek::Photo.new(app, width: 192, height: 192)
dest.put_zoomed_block(sprite_data, 64, 64, zoom_x: 3, zoom_y: 3)

See Also:

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(app, name: nil, width: nil, height: nil, file: nil, data: nil, format: nil, palette: nil, gamma: nil) ⇒ Photo

Create a new photo image.

The underlying Tk image is automatically deleted once this Photo object is garbage collected and nothing else references it - keep it around (an ivar, a collection, etc.) for as long as you need the image, the same contract as File or Socket. If only the image name is kept (e.g. passed into a widget's image:) and this wrapper is dropped, the image can be reclaimed out from under that reference; Tk shows a broken image rather than crashing, but the image won't come back - call #delete explicitly rather than relying on GC timing if you need deterministic cleanup.

Parameters:

  • app (Teek::App)

    the application instance

  • name (String, nil) (defaults to: nil)

    Tcl image name (auto-generated if nil)

  • width (Integer, nil) (defaults to: nil)

    image width in pixels

  • height (Integer, nil) (defaults to: nil)

    image height in pixels

  • file (String, nil) (defaults to: nil)

    path to an image file to load

  • data (String, nil) (defaults to: nil)

    base64-encoded image data

  • format (String, nil) (defaults to: nil)

    image format (e.g. "png", "gif")

  • palette (String, nil) (defaults to: nil)

    palette specification

  • gamma (Float, nil) (defaults to: nil)

    gamma correction value



101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
# File 'lib/teek/photo.rb', line 101

def initialize(app, name: nil, width: nil, height: nil,
               file: nil, data: nil, format: nil, palette: nil, gamma: nil)
  @app = app
  @name = name || self.class.next_name

  kwargs = {}
  kwargs[:width] = width if width
  kwargs[:height] = height if height
  kwargs[:file] = file if file
  kwargs[:data] = data if data
  kwargs[:format] = format if format
  kwargs[:palette] = palette if palette
  kwargs[:gamma] = gamma if gamma

  @app.command(:image, :create, :photo, @name, **kwargs)
  ObjectSpace.define_finalizer(self, self.class.finalizer_for(@name, @app))
end

Instance Attribute Details

#appObject (readonly)

Returns the value of attribute app.



43
44
45
# File 'lib/teek/photo.rb', line 43

def app
  @app
end

#nameObject (readonly)

Returns the value of attribute name.



43
44
45
# File 'lib/teek/photo.rb', line 43

def name
  @name
end

Class Method Details

.finalizer_for(name, app) ⇒ Object

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

Builds the proc registered as this instance's finalizer. Must not close over self (the Photo instance) or any of its ivars directly - a finalizer that references its own object keeps that object permanently reachable, so it would never actually be collected. +name+/+app+ are plain local captures instead.

Finalizers can run on any thread, so the delete goes through Interp#queue_for_main (fire-and-forget) rather than #tcl_eval, which would block on a cross-thread queue wait if called from a background thread - risky from inside a GC finalizer. The catch means a concurrent explicit #delete (or an interpreter that's already torn down by the time this runs) is a silent no-op, not an error with nowhere to go.



69
70
71
72
73
74
75
76
77
# File 'lib/teek/photo.rb', line 69

def finalizer_for(name, app)
  proc do
    begin
      app.interp.queue_for_main(proc { app.tcl_eval("catch {image delete #{name}}") })
    rescue Teek::TclError
      # interpreter already torn down - nothing to clean up
    end
  end
end

.next_nameObject

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.



49
50
51
52
# File 'lib/teek/photo.rb', line 49

def next_name
  @counter += 1
  "teek_photo#{@counter}"
end

Instance Method Details

#==(other) ⇒ Object Also known as: eql?



278
279
280
# File 'lib/teek/photo.rb', line 278

def ==(other)
  other.is_a?(Photo) ? @name == other.name : @name == other.to_s
end

#blankself Also known as: clear

Clear the image to fully transparent.

Returns:

  • (self)


241
242
243
244
# File 'lib/teek/photo.rb', line 241

def blank
  @app.interp.photo_blank(@name)
  self
end

#command(*args, **kwargs) ⇒ String

Invoke a photo subcommand not covered by a dedicated method above (e.g. copy, read, write). Prepends the image name as the Tcl command, exactly like Widget#command does for widgets.

Examples:

Subsample a copy of another photo

thumb = Teek::Photo.new(app)
thumb.command(:copy, source_photo, subsample: 4)

Parameters:

  • args

    positional arguments

  • kwargs

    keyword arguments mapped to -key value pairs

Returns:

  • (String)

    the Tcl result



129
130
131
# File 'lib/teek/photo.rb', line 129

def command(*args, **kwargs)
  @app.command(@name, *args, **kwargs)
end

#deletevoid

This method returns an undefined value.

Delete this photo image and free its resources.

Cancels the GC finalizer registered by #initialize - otherwise a later collection could delete an unrelated image if this name gets reused (e.g. an explicit name: passed to a later Photo).



255
256
257
258
# File 'lib/teek/photo.rb', line 255

def delete
  ObjectSpace.undefine_finalizer(self)
  @app.tcl_eval("image delete #{@name}")
end

#exist?Boolean

Check if this photo image still exists.

Returns:

  • (Boolean)


263
264
265
266
267
# File 'lib/teek/photo.rb', line 263

def exist?
  @app.tcl_eval("image type #{@name}") == 'photo'
rescue Teek::TclError
  false
end

#expand(width, height) ⇒ self

Note:

Has no effect on photos created with explicit width: / height: options. Only works on auto-sized photos (those whose size was set by writing pixel data). This is a Tk limitation.

Expand image to at least the given dimensions. Will not shrink.

Parameters:

  • width (Integer)

    minimum width

  • height (Integer)

    minimum height

Returns:

  • (self)


233
234
235
236
# File 'lib/teek/photo.rb', line 233

def expand(width, height)
  @app.interp.photo_expand(@name, width, height)
  self
end

#get_image(x: nil, y: nil, width: nil, height: nil, unpack: false) ⇒ Hash

Read pixel data from the image.

Parameters:

  • x (Integer) (defaults to: nil)

    source X offset

  • y (Integer) (defaults to: nil)

    source Y offset

  • width (Integer, nil) (defaults to: nil)

    region width (nil for full image)

  • height (Integer, nil) (defaults to: nil)

    region height (nil for full image)

  • unpack (Boolean) (defaults to: false)

    if true, return flat array of integers instead of binary string

Returns:

  • (Hash)

    { data: String, width: Integer, height: Integer } or { pixels: Array<Integer>, width: Integer, height: Integer } if unpack is true



189
190
191
192
193
194
195
196
# File 'lib/teek/photo.rb', line 189

def get_image(x: nil, y: nil, width: nil, height: nil, unpack: false)
  opts = { unpack: unpack }
  opts[:x] = x if x
  opts[:y] = y if y
  opts[:width] = width if width
  opts[:height] = height if height
  @app.interp.photo_get_image(@name, opts)
end

#get_pixel(x, y) ⇒ Array<Integer>

Read a single pixel.

Parameters:

  • x (Integer)

    X coordinate

  • y (Integer)

    Y coordinate

Returns:

  • (Array<Integer>)

    [r, g, b, a] values (0-255)



203
204
205
# File 'lib/teek/photo.rb', line 203

def get_pixel(x, y)
  @app.interp.photo_get_pixel(@name, x, y)
end

#get_sizeArray<Integer>

Get image dimensions.

Returns:

  • (Array<Integer>)

    [width, height]



210
211
212
# File 'lib/teek/photo.rb', line 210

def get_size
  @app.interp.photo_get_size(@name)
end

#hashObject



283
284
285
# File 'lib/teek/photo.rb', line 283

def hash
  @name.hash
end

#inspectObject



274
275
276
# File 'lib/teek/photo.rb', line 274

def inspect
  "#<Teek::Photo #{@name}>"
end

#put_block(pixel_data, width, height, x: 0, y: 0, format: :rgba, composite: :set) ⇒ self

Write RGBA pixel data to the image.

Parameters:

  • pixel_data (String)

    binary string, 4 bytes (RGBA) per pixel

  • width (Integer)

    width of the pixel block

  • height (Integer)

    height of the pixel block

  • x (Integer) (defaults to: 0)

    destination X offset

  • y (Integer) (defaults to: 0)

    destination Y offset

  • format (:rgba, :argb) (defaults to: :rgba)

    pixel format

  • composite (:set, :overlay) (defaults to: :set)

    compositing rule

Returns:

  • (self)


143
144
145
146
147
# File 'lib/teek/photo.rb', line 143

def put_block(pixel_data, width, height, x: 0, y: 0, format: :rgba, composite: :set)
  opts = { x: x, y: y, format: format, composite: composite }
  @app.interp.photo_put_block(@name, pixel_data, width, height, opts)
  self
end

#put_zoomed_block(pixel_data, width, height, x: 0, y: 0, zoom_x: 1, zoom_y: 1, subsample_x: 1, subsample_y: 1, format: :rgba, composite: :set) ⇒ self

Write RGBA pixel data with zoom and subsample.

Zoom replicates each pixel (zoom=3 makes each source pixel 3x3). Subsample skips source pixels (subsample=2 takes every other pixel).

Parameters:

  • pixel_data (String)

    binary string, 4 bytes (RGBA) per pixel

  • width (Integer)

    source width in pixels

  • height (Integer)

    source height in pixels

  • x (Integer) (defaults to: 0)

    destination X offset

  • y (Integer) (defaults to: 0)

    destination Y offset

  • zoom_x (Integer) (defaults to: 1)

    horizontal zoom factor

  • zoom_y (Integer) (defaults to: 1)

    vertical zoom factor

  • subsample_x (Integer) (defaults to: 1)

    horizontal subsample factor

  • subsample_y (Integer) (defaults to: 1)

    vertical subsample factor

  • format (:rgba, :argb) (defaults to: :rgba)

    pixel format

  • composite (:set, :overlay) (defaults to: :set)

    compositing rule

Returns:

  • (self)


166
167
168
169
170
171
172
173
174
175
176
177
178
# File 'lib/teek/photo.rb', line 166

def put_zoomed_block(pixel_data, width, height,
                     x: 0, y: 0, zoom_x: 1, zoom_y: 1,
                     subsample_x: 1, subsample_y: 1,
                     format: :rgba, composite: :set)
  opts = {
    x: x, y: y,
    zoom_x: zoom_x, zoom_y: zoom_y,
    subsample_x: subsample_x, subsample_y: subsample_y,
    format: format, composite: composite
  }
  @app.interp.photo_put_zoomed_block(@name, pixel_data, width, height, opts)
  self
end

#set_size(width, height) ⇒ self

Set image dimensions. May crop or add transparent pixels.

Parameters:

  • width (Integer)

    new width

  • height (Integer)

    new height

Returns:

  • (self)


219
220
221
222
# File 'lib/teek/photo.rb', line 219

def set_size(width, height)
  @app.interp.photo_set_size(@name, width, height)
  self
end

#to_sString

Returns the Tcl image name.

Returns:

  • (String)

    the Tcl image name



270
271
272
# File 'lib/teek/photo.rb', line 270

def to_s
  @name
end