package miaou-core

  1. Overview
  2. Docs
Miaou core/widgets (no drivers, no SDL)

Install

dune-project
 Dependency

Authors

Maintainers

Sources

v0.5.2.tar.gz
md5=60a3b9f181f24572a06a9492532bfdda
sha512=fcc35a275066be2900e6201782faf47503076fa4640f08cf78067835a6f447b74613009e55b2ac799adb7ca46f1bffa261fc5971753f2cc3c6bef327511c7ef6

doc/src/miaou_widgets_display/image_widget.ml.html

Source file image_widget.ml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
(*****************************************************************************)
(*                                                                           *)
(* SPDX-License-Identifier: MIT                                              *)
(* Copyright (c) 2025 Nomadic Labs <contact@nomadic-labs.com>                *)
(* Copyright (c) 2026 Mathias Bourgoin <mathias.bourgoin@atacama.tech>       *)
(*                                                                           *)
(*****************************************************************************)

module W = Widgets

type pixel = {r : int; g : int; b : int}

type t = {
  width : int;
  height : int;
  pixels : pixel array array;
  file_path : string option;
  mutable sdl_texture : Obj.t option;
      (* Cached SDL texture - stored as Obj.t to avoid tsdl dependency *)
}

let rgb_to_ansi_256 r g b =
  (* Convert RGB (0-255) to ANSI 256 color code *)
  if r = g && g = b then
    (* Grayscale *)
    if r < 8 then 16 else if r > 248 then 231 else 232 + ((r - 8) / 10)
  else
    (* Color cube: 16 + 36*r + 6*g + b where r,g,b are in 0-5 *)
    let r' = r * 6 / 256 in
    let g' = g * 6 / 256 in
    let b' = b * 6 / 256 in
    16 + (36 * r') + (6 * g') + b'

let pixel_to_ansi_fg pixel =
  Printf.sprintf "\027[38;5;%dm" (rgb_to_ansi_256 pixel.r pixel.g pixel.b)

let pixel_to_ansi_bg pixel =
  Printf.sprintf "\027[48;5;%dm" (rgb_to_ansi_256 pixel.r pixel.g pixel.b)

let ansi_reset = "\027[0m"

let create_from_rgb ~width ~height ~rgb_data () =
  let pixels =
    Array.init height (fun y ->
        Array.init width (fun x ->
            let offset = ((y * width) + x) * 3 in
            {
              r = Char.code (Bytes.get rgb_data offset);
              g = Char.code (Bytes.get rgb_data (offset + 1));
              b = Char.code (Bytes.get rgb_data (offset + 2));
            }))
  in
  {width; height; pixels; file_path = None; sdl_texture = None}

let load_from_file path ~max_width ~max_height () =
  try
    (* Load image using imagelib *)
    let img = ImageLib_unix.openfile path in
    let orig_width = img.width in
    let orig_height = img.height in

    (* Calculate scaled dimensions preserving aspect ratio *)
    (* Note: terminal chars are roughly 2:1 (height:width), so we adjust *)
    let term_max_height = max_height * 2 in
    (* double height since we use half-blocks *)
    let width_scale = float_of_int max_width /. float_of_int orig_width in
    let height_scale =
      float_of_int term_max_height /. float_of_int orig_height
    in
    let scale = min width_scale height_scale in
    let new_width = max 1 (int_of_float (float_of_int orig_width *. scale)) in
    let new_height = max 1 (int_of_float (float_of_int orig_height *. scale)) in

    (* Simple nearest-neighbor scaling *)
    let pixels =
      Array.init new_height (fun y ->
          Array.init new_width (fun x ->
              let src_x =
                min (orig_width - 1) (int_of_float (float_of_int x /. scale))
              in
              let src_y =
                min (orig_height - 1) (int_of_float (float_of_int y /. scale))
              in
              (* ImageLib read_rgba uses a continuation *)
              Image.read_rgba img src_x src_y (fun r g b _a -> {r; g; b})))
    in

    Ok
      {
        width = new_width;
        height = new_height;
        pixels;
        file_path = Some path;
        sdl_texture = None;
      }
  with e ->
    Error (Printf.sprintf "Failed to load image: %s" (Printexc.to_string e))

(* SDL rendering helper - uses Obj.magic to avoid compile-time tsdl dependency.
   This code only runs when SDL context is available at runtime. *)
let render_image_sdl_cached t ctx =
  let open Sdl_chart_context in
  let renderer = get_renderer ctx in
  let char_w = ctx.char_w in
  let char_h = ctx.char_h in
  let x_pixels = 10 * char_w in
  let y_pixels = ctx.y_offset + (5 * char_h) in
  let scale = 1 in
  let tex_width = t.width * scale in
  let tex_height = t.height * scale in

  (* Check if texture exists and is valid *)
  let needs_create =
    match t.sdl_texture with None -> true | Some _ -> false
  in

  (* Create texture if needed - this block uses dynamic SDL calls *)
  if needs_create then begin
    (* These functions are resolved at runtime when SDL driver is loaded *)
    let create_texture = Sdl_chart_context.Sdl_ops.create_texture in
    let set_render_target = Sdl_chart_context.Sdl_ops.set_render_target in
    let set_render_draw_color =
      Sdl_chart_context.Sdl_ops.set_render_draw_color
    in
    let render_fill_rect = Sdl_chart_context.Sdl_ops.render_fill_rect in

    match create_texture renderer tex_width tex_height with
    | None -> ()
    | Some texture ->
        set_render_target renderer (Some texture) ;
        for py = 0 to t.height - 1 do
          for px = 0 to t.width - 1 do
            let pixel = t.pixels.(py).(px) in
            set_render_draw_color renderer pixel.r pixel.g pixel.b 255 ;
            render_fill_rect renderer (px * scale) (py * scale) scale scale
          done
        done ;
        set_render_target renderer None ;
        t.sdl_texture <-
          Some
            (Obj.repr
               texture
             [@allow_forbidden "SDL texture stored without tsdl dependency"])
  end ;

  (* Render the texture if it exists *)
  match t.sdl_texture with
  | Some texture_obj ->
      let render_copy = Sdl_chart_context.Sdl_ops.render_copy in
      render_copy
        renderer
        (Obj.obj texture_obj [@allow_forbidden "recover typed SDL texture"])
        x_pixels
        y_pixels
        tex_width
        tex_height
  | None -> ()

let render ?(crop_center = 1.0) t ~focus:_ =
  (* Check if SDL rendering is available *)
  let ctx_status = Sdl_chart_context.get_context () in
  match ctx_status with
  | Some ctx ->
      (* SDL mode: use cached texture rendering *)
      render_image_sdl_cached t ctx ;

      (* Return placeholder that reserves space for the image *)
      let placeholder_lines = (t.height / 2) + 5 in
      let buf = Buffer.create (placeholder_lines * 2) in
      for i = 0 to placeholder_lines - 1 do
        if i > 0 then Buffer.add_char buf '\n'
      done ;
      Buffer.contents buf
  | None ->
      (* Terminal mode: use colored half-blocks *)
      let crop_width = int_of_float (float_of_int t.width *. crop_center) in
      let crop_height = int_of_float (float_of_int t.height *. crop_center) in
      let crop_x = (t.width - crop_width) / 2 in
      let crop_y = (t.height - crop_height) / 2 in

      let upper_half = if W.prefer_ascii () then "#" else "▀" in
      let full_block = if W.prefer_ascii () then "#" else "█" in

      let char_height = (crop_height + 1) / 2 in
      (* 2 pixels per char row *)
      let result_buf = Buffer.create (char_height * ((crop_width * 20) + 1)) in

      for char_y = 0 to char_height - 1 do
        if char_y > 0 then Buffer.add_char result_buf '\n' ;

        for x = 0 to crop_width - 1 do
          let pixel_y_upper = crop_y + (char_y * 2) in
          let pixel_y_lower = pixel_y_upper + 1 in
          let pixel_x = crop_x + x in

          let has_upper = pixel_y_upper < t.height && pixel_x < t.width in
          let has_lower = pixel_y_lower < t.height && pixel_x < t.width in

          match (has_upper, has_lower) with
          | false, false -> Buffer.add_char result_buf ' '
          | true, false ->
              (* Only upper pixel *)
              let pixel = t.pixels.(pixel_y_upper).(pixel_x) in
              Buffer.add_string result_buf (pixel_to_ansi_fg pixel) ;
              Buffer.add_string result_buf full_block ;
              Buffer.add_string result_buf ansi_reset
          | false, true ->
              (* Only lower pixel *)
              let pixel = t.pixels.(pixel_y_lower).(pixel_x) in
              Buffer.add_string result_buf (pixel_to_ansi_fg pixel) ;
              Buffer.add_string result_buf full_block ;
              Buffer.add_string result_buf ansi_reset
          | true, true ->
              (* Both pixels *)
              let upper_pixel = t.pixels.(pixel_y_upper).(pixel_x) in
              let lower_pixel = t.pixels.(pixel_y_lower).(pixel_x) in
              if upper_pixel = lower_pixel then (
                (* Same color, use full block *)
                Buffer.add_string result_buf (pixel_to_ansi_fg upper_pixel) ;
                Buffer.add_string result_buf full_block ;
                Buffer.add_string result_buf ansi_reset)
              else (
                (* Different colors, use half-block with fg+bg *)
                Buffer.add_string result_buf (pixel_to_ansi_fg upper_pixel) ;
                Buffer.add_string result_buf (pixel_to_ansi_bg lower_pixel) ;
                Buffer.add_string result_buf upper_half ;
                Buffer.add_string result_buf ansi_reset)
        done ;
        Buffer.add_string result_buf ansi_reset
        (* Ensure reset at end of line *)
      done ;

      Buffer.contents result_buf

let get_dimensions t = (t.width, t.height)

let get_file_path t = t.file_path

let get_pixel t ~x ~y =
  if x < 0 || x >= t.width || y < 0 || y >= t.height then
    invalid_arg
      (Printf.sprintf
         "get_pixel: coordinates (%d, %d) out of bounds (width=%d, height=%d)"
         x
         y
         t.width
         t.height)
  else t.pixels.(y).(x)

let get_pixels t = t.pixels

let render_sdl t ~x ~y ~scale =
  (* Returns image data for SDL rendering
     Format: (x, y, width, height, pixel_data)
     where pixel_data is the raw pixel array for direct SDL texture upload *)
  (x, y, t.width * scale, t.height * scale, t.pixels)

[@@@enforce_exempt] (* non-widget module *)