[Feat]: Image loading using ocaml-imagelib library#113
Conversation
|
Ohh this is a strange error for the build checks this is working locally for me but maybe that can also be attributed to the fact that I had vendored imagelib. Is there any way that you think we can make this work on CI? |
Yes - how do you think CI knows about the other things Claudius depends on @pawaskar-shreya? If you can find that then you can solve this :) |
mdales
left a comment
There was a problem hiding this comment.
Overall, loving this PR: good decision to bring in Imagelib to do the image decoding here rather than just relying on GIFLib, you've added images to the right places (as a primitive and as a draw function on frame buffer).
I have two things that I think you should explore here:
Firstly the scaling confused me in the API - I think the scaling is a good idea, and encourage you not to lose it, but should it be on Picture.t or should it be an argument to the drawing function? When I read the original API, my assumption was that having provided the scaling then when I call pixels I'd get the image already scaled. But that isn't the case, the scaling is only used by the drawing routine - on which case, perhaps we should just pass the scaling there as an argument? That would then make it easier to implement something like this: https://www.youtube.com/watch?v=_P00jYB81ZQ without having to create many copies of the Picture.
Secondly, and more importantly, this abstraction doesn't work if I load two or more images, because the way the palette system works. All the images will have a palette that starts at 0 and works up, and whilst I can change the palette, if I try to draw two images the one that is used second will have the winning palette, making the first image look odd.
I think given this limitation of Claudius is might be best to take image loading away from the user, and have Claudius do it, in a sort of similar way to how I built up a palette programmatically in my FunOCaml talk (https://github.com/mdales/funocaml24/blob/main/bin/main.ml#L146). It could perhaps work like this:
- Screen.create would take an optional list of image filenames, along with the user provided palette
- Inside Screen.create we load the images into a list of Picture.t, and each image in turn will get an offset to add to its palette definition
- The palette for Claudius is then a concatenation of the user provided palette (which might be empty if they just want to use the image's palette) concatenated with all the image palettes in turn.
- Screen can then have a
picturesproperty of typePicture.t arraythat returns the images in the same order that the file names were provided.
This means the user never has to do any palette manipulation by default (which I think new users won't want to worry about), and you can ensure that all loaded images have their own unique set of colours.
We already do this for fonts if you don't want to use the built in system font: you provide it to screen, and then when you want it later you call Screen.font. This also promotes lack of global variables which is a good thing in programming, and particularly in functional programming as global variables are effectively side effects of sorts.
How does this sound?
I know that's a lot of feedback, but overall I'm super happy with this technically, I just think the APIs need considering a little more.
| type t | ||
| (** Abstract type representing a loaded picture *) | ||
|
|
||
| val load : string -> float -> t |
There was a problem hiding this comment.
A couple of thoughts here:
Could we call this from_file just to keep it consistent with giflib?
I think the common case will be that most people leave the scale as 1.0, and so I'd advocate that the scale parameter is an optional parameter, and by default we don't do any scaling.
I'd also suggest the filename argument should be last so as to make it easier to do things like:
let img = get_user_input_of_filename () |> Picture.load| val scaled_width : t -> int | ||
| (** [scaled_width pic] returns the width in pixels after applying the scale. *) | ||
|
|
||
| val scaled_height : t -> int |
There was a problem hiding this comment.
For both this and the original sizes can we just have a single method that returns a tuple containing width and height? That then is consistent with Screen.dimensions.
| (** [set_scale pic s] returns a new picture with the scale factor updated to | ||
| [s]. *) | ||
|
|
||
| val scaled_width : t -> int |
There was a problem hiding this comment.
I think this should not need the scaled_ prefix - you set the scaling at load time, and then that's it, that's your image.
| val original_width : t -> int | ||
| (** [original_width pic] returns the original width in pixels. *) | ||
|
|
||
| val original_height : t -> int |
There was a problem hiding this comment.
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?
| (** [filled_polygon points colour framebuffer] Draws a filled polygon made from | ||
| the list of [points] in the specified [colour] into [framebuffer]. *) | ||
|
|
||
| val draw_picture : Picture.t -> int -> int -> t -> unit |
There was a problem hiding this comment.
draw_char and draw_string take the position as the first arguments - for consistency this should match those (I don't think they're better than what you've done - just we should be consistent).
| in | ||
|
|
||
| let palette_rgb_24 = | ||
| 0x000000 |
There was a problem hiding this comment.
Maybe add a comment about why you're doing this.
There was a problem hiding this comment.
Can we avoid this by using a sentinel value (say -1) in the pixel array rather than 0?
There was a problem hiding this comment.
I still think this isn't needed: If we have an invalid value rather than zero to represent transparent in the pixel array.
| let idx = (src_y * src_w) + src_x in | ||
| let color_index = pixels.(idx) in | ||
|
|
||
| if color_index <> 0 then |
There was a problem hiding this comment.
Perhaps a comment here as to why 0 is being special cased here
|
I had a brief panic that we would not be able to use Imagelib, as the opam info says it is licensed under GPL-3.0, and that is not compatible with the ICS/MIT style license Claudius uses. However, when I go to the project page, it is show as using the LGPL, which is okay: https://github.com/rlepigre/ocaml-imagelib/blob/master/LICENSE |
a5063b2 to
c4af62b
Compare
|
Heyy @mdales the builds are again failing, this time I have added imagelib and iamgelib.unix to both dune-project and opam. it just says dune not found I don't understand why this is the case. do I need to raise a seperate PR for these: imagelib and iamgelib.unix ; as even if these changes are added in this PR only, CI still follows the old CI workflow. But does that also seems contradicting as the dependencies are anyways fetched from opam file. does explicitly adding a dune install stanza in main.yml sound like a good option here? |
|
@pawaskar-shreya I don't understand why main builds and your PR does not, you've not done anything I can see to cause dune to fail. That said, we don't have dune as a dependancy. Can you add a stage to the CI script |
dune is present as a dependency in both dune-project and claudius.opam. So my thought was that it was fetched initially from there only in all our previous PRs. Dont know why it is not doing the same now.
Yupp! raising a PR! |
c4af62b to
2faaed8
Compare
mdales
left a comment
There was a problem hiding this comment.
Thanks for working with my requests! I think that change makes things more easier to use for new users and the common use case.
The one thing I think that isn't done in an idiomatic functional way is that you've added some global state for building up the palettes. You can do this without introducing global state I think if in the Screen.create function you replace the list.map with a list.fold of some sorts and as you load each image you work out its offset and then pass that as an optional argument to the next image load.
Otherwise I think the other small things are:
- You still have save separate width/height getters for the image rather than just a single call to get both
- No tests
- Still using a 0 for the sentinel transparent value, where as I think we should avoid using valid colours
Okayy yea, I should try this!
Yupp! I am yet to do a bit work here as right now the pixel bleeding problem is also sort of in my way. But yea, once that is done, I shall get these changes in too! |
|
Heyy @mdales I have made some of the changes:
But please do suggest me and give your feedback on the recent commits! |
b6a2031 to
e5fd6b6
Compare
mdales
left a comment
There was a problem hiding this comment.
Nicely done with the no globals!
I've marked this as approved, as we can merge this, however it would be nice if time allows to review the use of the 0 element in each image palette - but I can always fix that afterwards, because this already makes Claudius better, so I'm happy for it to merge if you need to focus on your presentation.
| (** [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 |
There was a problem hiding this comment.
Ideally we'd have something in test_palette.ml corresponding to this.
| in | ||
|
|
||
| let palette_rgb_24 = | ||
| 0x000000 |
There was a problem hiding this comment.
I still think this isn't needed: If we have an invalid value rather than zero to represent transparent in the pixel array.
|
|
||
| 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 |
There was a problem hiding this comment.
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 load_as_indexed?
| pixel_write fb_x fb_y color_index fb | ||
| done | ||
| done; | ||
| fb.dirty <- true |
There was a problem hiding this comment.
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 :)
| | 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 |
There was a problem hiding this comment.
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.
Helloww @mdales 👋👋
Here's PR for the image loading module
I have also written a test_picture module but havent added it here, as it is using an image, so I would like to have your opinion here on how to proceed.