-
Notifications
You must be signed in to change notification settings - Fork 14
[Feat]: Image loading using ocaml-imagelib library #113
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
4d68ca8
0632128
bb01b5a
6d7d354
4974885
b68d073
b7e2b9b
4860c8e
70e0c90
9f14347
ae077d4
836d05f
3f6b597
41f3533
7f9b297
a182a2b
acb9a2f
ba07cec
6215794
e1e1449
056bb50
e5fd6b6
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -461,6 +461,28 @@ let filled_polygon (points : (int * int) list) (col : int) (buffer : t) = | |
| map; | ||
| buffer.dirty <- true | ||
|
|
||
| let draw_picture (pic : Picture.t) ?(scale = 1.0) (offset_x : int) | ||
| (offset_y : int) (fb : t) : unit = | ||
| let src_w = Picture.original_width pic in | ||
| let src_h = Picture.original_height pic in | ||
| let dst_w = int_of_float (float src_w *. scale) in | ||
| let dst_h = int_of_float (float src_h *. scale) in | ||
| let pixels = Picture.pixels pic in | ||
| for y = 0 to dst_h - 1 do | ||
| for x = 0 to dst_w - 1 do | ||
| let src_x = min (src_w - 1) (int_of_float (float x /. scale)) in | ||
| let src_y = min (src_h - 1) (int_of_float (float y /. scale)) in | ||
| let idx = (src_y * src_w) + src_x in | ||
| let color_index = pixels.(idx) in | ||
|
|
||
| if color_index <> 0 then | ||
| let fb_x = x + offset_x in | ||
| let fb_y = y + offset_y in | ||
| pixel_write fb_x fb_y color_index fb | ||
| done | ||
| done; | ||
| fb.dirty <- true | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This isn't needed now as you call pixel_write, which itself will set the dirty flag. Not bad, just less code is always good :) |
||
|
|
||
| (* ----- *) | ||
|
|
||
| let draw_char (x : int) (y : int) (f : Font.t) (c : char) (col : int) | ||
|
|
@@ -559,7 +581,9 @@ let render (buffer : t) (draw : Primitives.t list) = | |
| | Primitives.Char (p, font, c, col) -> | ||
| ignore (draw_char p.x p.y font c col buffer) | ||
| | Primitives.String (p, font, s, col) -> | ||
| ignore (draw_string p.x p.y font s col buffer)) | ||
| ignore (draw_string p.x p.y font s col buffer) | ||
| | Primitives.Picture (pos, pic) -> | ||
| draw_picture pic ~scale:1.0 pos.x pos.y buffer) | ||
| draw | ||
|
|
||
| (* ----- *) | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -83,3 +83,6 @@ val circle_palette : t -> int -> t | |
| val updated_entry : t -> int -> int * int * int -> t | ||
| (** [updated_entry pal index new_color] checks for the index then returns a new | ||
| palette with the entry at [index] updated to [new_color]. *) | ||
|
|
||
| val concat : t list -> t | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Ideally we'd have something in test_palette.ml corresponding to this. |
||
| (** [concat palettes] merges a list of palettes into a single palette. *) | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,83 @@ | ||
| open Image | ||
|
|
||
| type t = { palette : Palette.t; pixels : int array; width : int; height : int } | ||
|
|
||
| let load_png_as_indexed (filepath : string) : Palette.t * int array * int * int | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Whilst you're only testing it for PNG, doesn't using ImageLib here mean you can load any image that ImageLib supports? Nothing here seems PNG specific. Can this just become |
||
| = | ||
| let img = ImageLib_unix.openfile filepath in | ||
| let w = img.width in | ||
| let h = img.height in | ||
|
|
||
| let pixels_rgba = | ||
| Array.init (w * h) (fun idx -> | ||
| let x = idx mod w in | ||
| let y = idx / w in | ||
| match img.pixels with | ||
| | RGB (r, g, b) -> | ||
| let red = Pixmap.get r x y in | ||
| let green = Pixmap.get g x y in | ||
| let blue = Pixmap.get b x y in | ||
| (red, green, blue, 255) | ||
| (* 255 means fully opaque *) | ||
| | RGBA (r, g, b, a) -> | ||
| let red = Pixmap.get r x y in | ||
| let green = Pixmap.get g x y in | ||
| let blue = Pixmap.get b x y in | ||
| let alpha = Pixmap.get a x y in | ||
| (red, green, blue, alpha) | ||
| | Grey p -> | ||
| let g = Pixmap.get p x y in | ||
| (g, g, g, 255) | ||
| | GreyA (p, a) -> | ||
| let g = Pixmap.get p x y in | ||
| let alpha = Pixmap.get a x y in | ||
| (g, g, g, alpha)) | ||
| in | ||
|
|
||
| let module ColorMap = Map.Make (struct | ||
| type t = int * int * int | ||
|
|
||
| let compare = compare | ||
| end) in | ||
| let palette_map, palette_list, _ = | ||
| Array.fold_left | ||
| (fun (map, lst, idx) (r, g, b, a) -> | ||
| if a = 0 then (map, lst, idx) (* transparent pixel *) | ||
| else if ColorMap.mem (r, g, b) map then (map, lst, idx) | ||
| else (ColorMap.add (r, g, b) idx map, lst @ [ (r, g, b) ], idx + 1)) | ||
| (ColorMap.empty, [], 1) (* index 0 is being used for transparency *) | ||
| pixels_rgba | ||
| in | ||
|
|
||
| let palette_rgb_24 = | ||
| 0x000000 | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Maybe add a comment about why you're doing this.
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Can we avoid this by using a sentinel value (say -1) in the pixel array rather than 0?
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I still think this isn't needed: If we have an invalid value rather than zero to represent transparent in the pixel array. |
||
| :: List.map (fun (r, g, b) -> (r lsl 16) lor (g lsl 8) lor b) palette_list | ||
| in | ||
|
|
||
| let pal = Palette.of_list palette_rgb_24 in | ||
|
|
||
| let indexed_pixels = | ||
| Array.map | ||
| (fun (r, g, b, a) -> | ||
| if a = 0 then 0 else ColorMap.find (r, g, b) palette_map) | ||
| pixels_rgba | ||
| in | ||
|
|
||
| (pal, indexed_pixels, w, h) | ||
|
|
||
| (* Public API, so real img data isn't tampered *) | ||
|
|
||
| let load (filepath : string) : t = | ||
| let palette, pixels, w, h = load_png_as_indexed filepath in | ||
| { palette; pixels; width = w; height = h } | ||
|
|
||
| let original_width (pic : t) = pic.width | ||
| let original_height (pic : t) = pic.height | ||
| let pixels (pic : t) = pic.pixels | ||
| let palette (pic : t) = pic.palette | ||
|
|
||
| let with_palette_offset (pic : t) (offset : int) : t = | ||
| let shifted_pixels = | ||
| Array.map (fun idx -> if idx = 0 then 0 else idx + offset) pic.pixels | ||
| in | ||
| { pic with pixels = shifted_pixels } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,21 @@ | ||
| type t | ||
| (** Abstract type representing a loaded picture *) | ||
|
|
||
| val load : string -> t | ||
| (** [load filename] loads a PNG file and returns a picture. *) | ||
|
|
||
| val original_width : t -> int | ||
| (** [original_width pic] returns the original width in pixels. *) | ||
|
|
||
| val original_height : t -> int | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I understand why you added this, but when designing an API one should keep it as small as possible, as then there is less to test, less to maintain, etc. What use is the original size to the user? surely they only need to know the size it will be drawn at? |
||
| (** [original_height pic] returns the original height in pixels. *) | ||
|
|
||
| val pixels : t -> int array | ||
| (** [pixels pic] returns the indexed pixel array. *) | ||
|
|
||
| val palette : t -> Palette.t | ||
| (** [palette pic] returns the color palette of the picture. *) | ||
|
|
||
| val with_palette_offset : t -> int -> t | ||
| (** [with_palette_offset pic offset] returns a new picture with all pixel | ||
| indices shifted by [offset]. Palette is unchanged. *) | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -18,3 +18,4 @@ type t = | |
| | FilledTriangle of point * point * point * int | ||
| | Char of point * Font.t * char * int | ||
| | String of point * Font.t * string * int | ||
| | Picture of point * Picture.t | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This ideally would have a scale on it - I realised I couldn't use the primitives for the flying ocaml logo example because there is no scale argument here. |
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,74 @@ | ||
| open OUnit2 | ||
| open Claudius | ||
| open Picture | ||
|
|
||
| let test_valid_png _ = | ||
| (* Credits for tetris.png: https://publicdomainvectors.org/en/free-clipart/3D-Tetris-blocks-vector-illustration/6089.html *) | ||
| let pic = load "../test_assets/tetris.png" in | ||
| assert_bool "width > 0" (original_width pic > 0); | ||
| assert_bool "height > 0" (original_height pic > 0); | ||
| assert_bool "has pixels" (Array.length (pixels pic) > 0) | ||
|
|
||
| let test_scaled_dimensions _ = | ||
| let pic = load "../test_assets/tetris.png" in | ||
| let w = original_width pic in | ||
| let h = original_height pic in | ||
| assert_equal w (Array.length (pixels pic) / h) | ||
|
|
||
| let test_draw_picture_normal _ = | ||
| let pic = load "../test_assets/tetris.png" in | ||
| let pal = palette pic in | ||
| assert_bool "palette has entries" (Palette.size pal > 0); | ||
| assert_bool "pixels reference palette" (pixels pic |> Array.exists (( <> ) 0)) | ||
|
|
||
| let test_draw_picture_negative_offset _ = | ||
| let pic = load "../test_assets/tetris.png" in | ||
| let shifted = with_palette_offset pic (-1) in | ||
| Array.iteri | ||
| (fun i idx -> | ||
| if idx = 0 then assert_equal 0 (pixels shifted).(i) | ||
| else assert_equal (idx - 1) (pixels shifted).(i)) | ||
| (pixels pic) | ||
|
|
||
| let test_draw_picture_scaled _ = | ||
| let pic = load "../test_assets/tetris.png" in | ||
| let w = original_width pic in | ||
| let h = original_height pic in | ||
| assert_equal (w * h) (Array.length (pixels pic)) | ||
|
|
||
| let test_load_png_as_indexed_transparent _ = | ||
| let pic = Picture.load "../test_assets/tetris.png" in | ||
| let pal = Picture.palette pic in | ||
| let pixels = Picture.pixels pic in | ||
| let w = Picture.original_width pic in | ||
| let h = Picture.original_height pic in | ||
| assert_bool "image has width > 0" (w > 0); | ||
| assert_bool "image has height > 0" (h > 0); | ||
| (* palette[0] reserved for transparency *) | ||
| assert_equal 0x000000l (Palette.index_to_rgb pal 0); | ||
| assert_bool "transparent pixel present" (Array.exists (( = ) 0) pixels) | ||
|
|
||
| let test_with_palette_offset _ = | ||
| let pic = load "../test_assets/tetris.png" in | ||
| let shifted = with_palette_offset pic 10 in | ||
| Array.iteri | ||
| (fun i idx -> | ||
| if idx = 0 then assert_equal 0 (pixels shifted).(i) | ||
| (* transparency stays 0 *) | ||
| else assert_equal (idx + 10) (pixels shifted).(i)) | ||
| (pixels pic) | ||
|
|
||
| let suite = | ||
| "Picture tests" | ||
| >::: [ | ||
| "valid_png" >:: test_valid_png; | ||
| "scaled_dimensions" >:: test_scaled_dimensions; | ||
| "draw_picture_normal" >:: test_draw_picture_normal; | ||
| "draw_picture_negative_offset" >:: test_draw_picture_negative_offset; | ||
| "draw_picture_scaled" >:: test_draw_picture_scaled; | ||
| "load_png_as_indexed transparent" | ||
| >:: test_load_png_as_indexed_transparent; | ||
| "with_palette_offset" >:: test_with_palette_offset; | ||
| ] | ||
|
|
||
| let () = run_test_tt_main suite |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,4 @@ | ||
| [ZoneTransfer] | ||
| ZoneId=3 | ||
| ReferrerUrl=https://publicdomainvectors.org/en/free-clipart/3D-Tetris-blocks-vector-illustration/6089.html | ||
| HostUrl=https://publicdomainvectors.org/photos/Tetris_block.png |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Perhaps a comment here as to why 0 is being special cased here