geometric and color transforms, Linux testing system

This commit is contained in:
Lephenixnoir 2020-03-09 18:13:32 +01:00
commit 0fb79006f2
22 changed files with 1398 additions and 0 deletions

4
.gitignore vendored Normal file
View File

@ -0,0 +1,4 @@
# Testing build system
/testing/libimg
/testing/transform.bin
/testing/transform.png

328
README.md Normal file
View File

@ -0,0 +1,328 @@
# libimg: Image transform and blending for gint
This library contains a variety of function to perform blending, geometric and color transforms on images in [gint](/Lephenixnoir/gint), namely:
* Extract references to subimages
* Blending with alpha (only full opaque and full transparent)
* Horizontal and vertical flip (mirror)
* Rotation by 90, 180 and 270 degrees
* Upscaling by integer factors
* Fast brightening and darkening
* Some visual effects that look cool and are cheap to compute.
![(Sample image showcasing the capabilities of libimg.)](testing/transform.png)
## Differences between `img_t` and `image_t`
The type of images in this library is called `img_t`. It is **not the same as `image_t`** (and `image_t` is the one that should be renamed to avoid the confusion).
* `image_t` objects are used by a gint component called *bopti* which specializes in maximizing rendering performance. They are very fast, especially on fx-9860G, but not flexible. They should be probably be called `bopti_image_t`.
* `img_t` objects are part of *libimg*. They're not rendered as fast, but they are more versatile and can be transformed in many ways.
There is no function to convert between `image_t` and `img_t`. Both types of images are generated by fxconv at compile time. In the unlikely event that an image needs to be available in both formats, the proper way is to convert it twice with different parameters. But normally, only one format is needed:
* If the image needs to be read or transformed, use libimg. Set the fxconv parameters of the resource to `type:libimg_image` and declare it as `extern img_t myimage`.
* Otherwise, bopti should be used for maximum performance. Set the fxconv parameters to `type:bopti_image` and declare it as `extern image_t myimage`.
## Compiling libimg
TODO
## Converting images and rendering to VRAM
The first step to use libimg in a program is to convert assets to the `img_t` format. This is done by setting the `type` parameter of the resource to `libimg_image`. For example:
```
IMG.sprite.png = type:libimg_image name:sprite
```
Then the newly-converted image can be accessed from a C program by extern-declaring as ny other fxconv resource. It is ready to use immediately. The definition of `img_t` and the libimg functions are provided by `<libimg.h>`, which needs to be included.
```
#include <libimg.h>
extern img_t const sprite;
```
Note that **converted images are read-only**, as is everything generated by fxconv. This means that the declared image cannot be written to or transformed in-place. But more on that later. To remember this read-only status, I added the `const` keyword; this is purely optional.
To render this shiny image to VRAM, use `img_render_vram()`. On fx-9860G (Graph 35+E family), this is the only way to render to VRAM because the VRAM has a different format than `img_t`. On fx-CG 50 (Graph 90+E), more powerful methods are available.
```
img_render_vram(sprite, <x>, <y>);
```
`img_render_vram()`, as every other libimg function, accounts for transparent pixels. Only opaque pixels will be copied to VRAM. This makes it very easy to blend images together by just rendering in sequence.
## Creating and destroying new images
Because our converted sprite is read-only, we can't do much more for now. To start working on transforms and cool animations, we need to create new images that can be written to. There are two ways to do this; the first is to create an empty image of a fixed size, and the second is to duplicate an existing image.
`img_create()` will create a new uninitialized image. Note that the value of the pixels is completely random at this point. We'll see how to fill the whole image with `img_fill()` later. Sometimes filling is not needed and would only decrease performance, so libimg tries to not get in your way and does not do it automatically.
```
img_t new_32x48_image = img_create(32, 48);
```
`img_copy()` will create a copy of an image. The returned copy will be of the same size as the source, but with duplicated pixels. A copy can always be written to, even if the source is read-only.
```
img_t copy_of_sprite = img_copy(sprite);
```
Of course, creating new images *can fail*. Both functions use `malloc()` under-the-hood, and `malloc()` fails if there is not enough memory. In this case, `img_create()` and `img_copy()` return a *null image*, which is like the `NULL` pointer for images. To see if an image is null, call `img_null()`.
```
if(img_null(copy_of_sprite)) {
/* Argh, copy has failed! */
}
else {
/* Everything is fine, let's go! */
}
```
A null image does not have pixels, and reading or writing it would be an error. You can use a null image in libimg functions though; they will realize the problem and either do nothing or return another null image.
Also, because there is an `malloc()`, there has to be a `free()`. Images created by `img_create()` and `img_copy()` need to be destroyed once they are no longer needed. Calling `img_destroy()` will do just that.
```
img_destroy(new_32x48_image);
img_destroy(copy_of_sprite);
```
Note that converted images such as `sprite`, null images and subimages (described later) don't need to be destroyed because they are not created with `malloc()`. To make life easier, `img_destroy()` can tell them apart, so when in doubt just destroy everything after use.
## Basic transforms
Let's start playing with these new shiny images. We'll start by flipping the sprite horizontally to simulate walking in the other direction. Let's make a new image of the required size and fill it with white pixels. This is important, because a sprite has transparent pixels, so if we don't clear the new image, random pixels will be visible around the sprite.
```
#include <gint/display.h>
img_t flipped_sprite = img_create(sprite.width, sprite.height);
img_fill(flipped_sprite, C_WHITE);
```
This example introduces several useful elements:
* The dimension of an image can be accessed by accessing its `width` and `height` attributes.
* The `img_fill()` function will paint all the pixels of an image in a uniform color.
* Colors used in libimg are the same as colors used in gint, except for transparency on fx-CG 50 (Graph 90+E), which is `0x0001`. More on that soon.
Now we've got an blank image with the same size as `sprite`, so we can go ahead and flip `sprite` horizontally into it:
```
img_hflip(sprite, flipped_sprite);
```
That's all. Now `img_render_vram(flipped_sprite, <x>, <y>)` will show the flipped version. As long as `flipped_sprite` is not destroyed, it can be rendered an unlimited amount of times. (Note that the transform wouldn't work if `flipped_sprite` were read-only. We couldn't use `sprite` as a target, for instance.)
But wait, since `flipped_sprite` was originally white, this just made a horrible white rectangle on the screen. What we actually wanted is to make the background transparent. We can use the special color `IMG_ALPHA` to do this.
```
/* Makes flipped_sprite completely transparent: */
img_fill(flipped_sprite, IMG_ALPHA);
```
On fx-9860G (Graph 35+E family), libimg colors are exactly the same as gint colors, so `IMG_ALPHA` is equivalent to `C_NONE`. On fx-CG 50 (Graph 90+E), things are a bit different. Colors are in the RGB565 format, and all 16-bit values are valid RGB565 colors, so there is no room for a special transparent color. libimg solves this dilemma by deciding that `0x0001` represents transparency. `0x0001` has been chosen because it is an extremely dark color, undistinguishable from black, and it's easy to compare numbers to `1` so it's technically faster than other alternatives. Just remember than `IMG_ALPHA` should be used to talk about transparency in libimg.
Because filling a new image with transparent pixels is a common operation, a shortcut called `img_clear()` is provided.
```
/* Also makes flipped_sprite completely transparent: */
img_clear(flipped_sprite);
```
So now the code does `img_create()`, `img_clear()`, and `img_hflip()` as a transform. This is also a common sequence, so a shortcut called `img_hflip_create()` is provided to do exactly that.
```
img_t flipped_sprite = img_hflip_create(sprite);
```
All transforms work like this. This includes:
* `img_hflip()` and `img_vflip()`, which flip horizontally and vertically.
* `img_rotate()`, which rotates by 0, 90, 180 or 270 degrees.
* `img_upscale()` which scales up by integer factors.
* `img_dye()` which paints all non-transparent pixels in a single color.
* `img_lighten()`, `img_whiten()` and `img_darken()` which make colors lighter or darker.
We can already mention a few conventions within the library:
* All transforms first take the `src` (source image) and `dst` (destination images) parameters, then any other parameter of the transform (such as the rotation angle).
* All transforms have a `_create()` variant which does not have a `dst` argument. The destination image is created automatically with just the size of the transformed result and returned.
## Subsurfaces and positioning
So far we have only transformed images to destinations of the same size, or in some cases such as rotation and upscaling, of related sizes. Let's say we want to create a full spritesheet with both the original and flipped version of `sprite`. We have a problem because `img_hflip()` does not allow us to tell where to write the result in the destination image. In fact, `img_hflip()` always writes the result in the top-left corner.
This is because libimg has a flexible positioning system that is more powerful than that method. This positioning system is built on the idea of **extracting references to subimages**.
Let's see an exemple. We'll create a spritesheet of size 32x16 with one 16x16 sprite on the left and one on the right.
```
img_t spritesheet = img_create(32, 16);
```
The right sprite is at position (16,0) within `spritesheet`. If we want to apply a lot of transforms on it, we'll have to keep mentioning these coordinates over and over, which is painful and error-prone. Instead, we can use a function called `img_sub()` to create a reference to the right half of `spritesheet`.
```
img_t right_sprite = img_sub(spritesheet, 16, 0, 16, 16);
```
`img_sub()` takes five parameters: the source image, and then the position and size of the subimage of interest, as `x`, `y`, `w`, and `h`. The right sprite starts at `x=16` and `y=0`, and is of width `w=16` and height `h=16`.
`img_sub()` **does not create a new image**. It only gives a new view into the same pixels. Drawing to `right_sprite` right now will totally draw into the right half of `spritesheet`! In fact, let's just do this.
```
img_fill(right_sprite, C_BLACK);
```
And just like that, we have filled *only half* of `spritesheet`. Calling `img_fill(spritesheet, C_BLACK)` would have filled *all of it*. See how the positioning system makes `img_fill()` as versatile as a fill-rectangle function.
With that in mind, we can now create the full spritesheet.
```
/* Copy the normal sprite */
img_render(sprite, spritesheet);
/* Flip into the right half */
img_hflip(sprite, right_sprite);
```
Of course, this works with all transforms. Please remember though that the destination image or subimage of a transform needs to be *at least as large* as the transformed result. If the destination is smaller, the transform will not happen.
The subimage does not create new pixels, so it does not need to be destroyed. `img_destroy()` can still be called on it and do nothing. But not destroying it means we don't have to store it in a variable, so we create the flipped sprite like this:
```
img_hflip(sprite, img_sub(spritesheet, 16, 0, 16, 16));
```
This reads as "flip `sprite` horizontally, and write the result in `spritesheet`, at position (16,0) in a rectangle of size 16x16".
In this case we know that the size of the rectangle *has* to be 16x16, because it's the size of the sprite. So we can omit it by setting the width and height to `-1`. This will create a reference to the subimage that starts at position (16,0) of `spritesheet` and extends all the way to the bottom right corner. `img_hflip()` will still only touch the first 16x16 pixels in the top left corner, so it's not a problem if the reference is larger than needed.
```
img_hflip(sprite, img_sub(spritesheet, 16, 0, -1, -1));
```
If you feel like this is a common construction, then you're absolutely right! So a shortcut has been made for it. Instead of setting the width and height to -1 we can just use `img_at()` which will set them for us. `img_at(img, x y)` is the same as `img_sub(img, x, y, -1, -1)`. So the transform becomes:
```
img_hflip(sprite, img_at(spritehseet, 16, 0));
```
This reads as "flip `sprite` horizontally and put the result in `spritesheet` at position (16,0)". It's much shorter than before, and because the reference to subimage does not need to be destroyed, there is no risk of leaking memory.
References to subimages are powerful and can be used in a variety of ways. But there is a golden rule: **destroying an image also destroys the subimages**. Once we call `img_destroy(spritesheet)`, all subimages including `right_sprite` become *invalid* and using them is an error. Always keep an eye on the full versions!
As an example of versatility of references to subimages, see how `img_sub()` can be used on the *source image* to transform only a part of it!
I have not yet explained what `img_render()` does, so let's take some time do discuss it.
## Direct rendering to VRAM (fx-CG 50)
On fx-9860G, the VRAM does not have the same format as an `img_t`, so we can't use the VRAM as the destination for a transform. We can only use `img_render_vram()` to copy an image to VRAM after it has been prepared.
But on fx-CG 50, the VRAM does have the same format as an `img_t`. Use `img_vram()` to get a reference to it:
```
img_t vram_as_an_image = img_vram();
```
Because it is only a reference, it does not need to be destroyed with `img_destroy()`. Doing it anyway is a no-operation, as with all other references. When in doubt, destroy everything after use.
This VRAM reference allows to directly transform to VRAM without using temporary images. For instance, we can do:
```
img_hflip(sprite, img_at(img_vram(), <x>, <y>));
```
Similarly, the following calls both fill a rectangle of the VRAM (although `drect()` is faster).
```
#include <gint/display.h>
drect(x, y, w, h, color);
img_fill(img_sub(img_vram(), x, y, w, h), color);
```
This is where the `img_render()` function becomes the most useful. This function copies its source to the top-left corner of its destination, without transforming it. Unlike transforms, it clips, so if the destination is smaller than the source, only the visible part will be copied. (Transforms do nothing in this situation.) In fact, on fx-CG 50, `img_render_vram()` is just a simple form for:
```
img_render(<src>, img_at(img_vram(), <x>, <y>));
```
`img_render()` is used most of the time in this way, to copy (render) images to VRAM. This is why it is called this way.
## In-place transforms
Some transforms can operate on a single image without requiring to have a different destination. The image is overwritten during the transform, so the original is lost but no additional meomry is needed. This is called an *in-place transform*.
In-place transforms are possible only when the size of the input is the same as the size of the output. Currently, this includes everything except rotation by 90 or 270 degrees, and upscaling by a non-trivial factor.
To perform an in-place transform, just use the same source and destination:
```
img_t sprite_copy = img_copy(sprite);
img_hflip(sprite_copy, sprite_copy);
```
It is also possible to use a section of an image as the source and another section as the target. The following class horizontally flips the left half of `spritesheet` into the right half:
```
img_hflip(img_sub(spritesheet, 0, 0, 16, 16), img_at(spritesheet, 16, 0));
```
This can also be performed directly on the VRAM on fx-CG 50 since the VRAM is an `img_t` in disguise.
Note however that performing a transform if the source and destination partially overlap (ie. they are not disjoint and they are not the same exact area either) is not supported for any transform and will always give an incorrect result!
## Accessing image data and custom transforms
Every image's pixels can be read manually, and also written if the image is not read-only. `img_t` is a structure with a description of the image, and the following useful attributes:
* `img.width` and `img.height` are the width and height of the image.
* `img.stride` is the number of pixels on each row. Most of the time this is larger than `img.width`!
* `img.pixels` is the pixel array.
The format is row-major order with stride. Here is how a 8x3 image with stride 12 looks like in memory. The numbers in the diagram correspond to indices in `img.pixels`.
<------ img.width ------>
+-------------------------+-------------+
| 0 1 2 3 4 5 6 7 | . . . . | ^
| 12 13 14 15 16 18 18 19 | . . . . | img.height
| 24 25 26 27 28 29 etc | . . . . | v
+-------------------------+-------------+
<------------ img.stride ------------->
For instance, the second pixel of second row is `img.pixels[13]`. See how one needs to add `img.stride` to the index to go down one line, instead of `img.width`. Also, indices represented by dots such as `pixesl[8]` are *not part of the image* and should never be accessed.
The reason with this format with stride is used is to support references to subimages. When creating a spritesheet of size 32x16, the stride is set to 32. And when we extract a 16x16 half, even though we are only considering half of each row, the rows are still 32 pixels apart from each other! This is why the stride of an image is often not equal to its width.
This also means that images are not contiguous in memory, so it is not possible to replace all the pixels in a single call to `memcpy()`.
Here is how one would iterate on all the pixels of an image. The following piece of code turns all transparent pixels white:
```
img_pixel_t *px = img.pixels;
for(int y=0; y<img.height; y++)
{
for(int x=0; x<img.width; x++)
{
/* Do something with px[x] */
if(px[x] == IMG_ALPHA) px[x] = C_WHITE;
}
px += img.stride;
}
```
Pixels are of type `img_pixel_t`. The way to manipulate them depends on the platform:
* On fx-9860G, `img_pixel_t` is `uint8_t` and its values are the colors from `<gint/display.h>`. The transparent color `IMG_ALPHA` is equal to `C_NONE`.
* On fx-CG 50, `img_pixel_t` is `uint16_t` and its values are RGB565 colors. Opaque colors from `<gint/display.h>` can be used, except for `IMG_ALHA = 0x0001` which represents transparency. Note that `IMG_ALPHA` is *not* equal to `C_NONE`.
## Issues and contributing
Bug reports and additions to the library (whethers issues or PRs) are very welcome. Since this is a static and overall simple library, I don't mind adding whichever transform is useful to game developers.
This repository uses a simple testing system for PC. Make from the `testing` library to produce a `libimg` executable that reads RGB565 bitmap files into `img_t` images on Linux. Use `make all` to also generate the `transform.png` file that is shown at the top of this README.

304
libimg.h Normal file
View File

@ -0,0 +1,304 @@
#ifndef LIBIMG_H
#define LIBIMG_H
#include <gint/display.h>
#include <stdint.h>
#ifdef FX9860G
/* On fx-9860G, the format is 8-bit per color. Although there are much less
than 256 colors (you could find 9 at best including partially-transparent
ones), it makes it possible to share code with the fx-CG 50, which is the
main target for this library. */
typedef uint8_t img_pixel_t;
#define IMG_ALPHA C_NONE
#endif /* FX9860G */
#ifdef FXCG50
/* On fx-CG 50, the format is 16-bit R5G6B5A. A value is reserved for
transparency; currently this is 0x0001. */
typedef uint16_t img_pixel_t;
#define IMG_ALPHA 0x0001
#endif /* FXCG50 */
/* The type of images; this is usually passed by value. The structure itself
only references the memory. Because it is passed by value, const qualifiers
are useless here (since the pixel pointer does not inherit them). */
typedef struct {
/* Image dimensions */
uint16_t width;
uint16_t height;
/* Number of pixels on each storage line */
uint16_t stride;
/* Flags (some internal) */
uint8_t flags;
/* Reserved for future use */
uint8_t _;
/* Pointer to pixels */
img_pixel_t *pixels;
} img_t;
//---
// Surface creation and destruction
//---
/* img_create(): Create a new, uninitialized image
This function creates a new surface and returns it. This surface must be
destroyed with img_destroy() when it is no longer needed. Sub-surfaces can
be created from it, but will no longer be usable when it is destroyed.
If the allocation fails, or the requested size is larger than 65535x65535,
this function returns a surface with zero dimensions and a NULL pixel
pointer. img_null() will return 1 on this error-image.
After creation, the image contains random pixels, which might not even be
valid colors on fx-9860G. It is advised to fill it with img_fill() unless
you know it will be filled by another mechanism such as a transform.
@w @h Dimensions of the new image */
img_t img_create(int w, int h);
/* img_copy(): Create a new image by copying an existing one
This function creates a new surface from a copy of the argument. Much like
with img_create(), the new surface must be destroyed with img_destroy(). */
img_t img_copy(img_t img);
/* img_null(): Check if an image is null (ie. failed to allocate)
A null image does not contain any data. It is correct to pass a null image
to any function of the library, but either nothing will happen or another
null image will be returned. */
int img_null(img_t img);
/* img_sub(): Get a reference to a section of an image
This function returns a surface that references a rectangle inside the
provided image [img]. This is *not* a new image; it is a view on a rectangle
inside [img]; just the same pixels viewed differently. It needs not be
destroyed, and it becomes invalid when [img] is destroyed. If you need an
actual copy, do img_copy(img_sub(img,x,y,w,h)).
The specified rectangle is clipped to fit into [img] before the reference is
created. If w or h is set to -1, then the full width or height of the image
is used.
@img Source image
@x @y Location of the top-left corner of the rectangle to reference
@w @h Size of the referenced rectangle */
img_t img_sub(img_t img, int x, int y, int w, int h);
/* img_at(): Get a reference to a section defined by its top-left corner
This function returns a sub-image of its first argument. The sub-image
starts at position (x,y) of the source image and ends in the bottom-right
corner.
This function is useful when transforming. This is because the result of a
transform is always drawn at position (0,0) on the destination image. To
change the position, one can get a reference using img_at().
For instance, the following call flips src horizontally and draws the result
onto dst at position (0,0):
img_hflip(src, dst);
To draw the flipped src at position (20,10), the call can be modified to:
img_hflip(src, img_at(dst, 20, 10));
If there is not enough space at position (20,10) of dst to draw the entirety
of the transformed src, the transform will fail and do nothing.
Calling img_at() is strictly equivalent to calling img_sub() with w=h=-1.
@img Source image
@x @y Location of the top-left corner to reference */
img_t img_at(img_t img, int x, int y);
#ifdef FXCG50
/* img_vram(): Obtain a reference to the current VRAM as an image
This function returns a reference to gint's VRAM wrapped into a surface
object. This is very useful to render to VRAM with all of the transforming
power of libimg surfaces. This surface needs not be destroyed.
This function is not available on fx-9860G because the format of the VRAM is
not compatible with that of surfaces. However, it is still possible to
render surfaces to VRAM using img_render_vram().
On fx-CG 50, the VRAM address changes at every frame when triple buffering
is active. It is recommended to call img_vram() at every frame to obtain the
correct address every time. (This function always executes instantly.) */
img_t img_vram(void);
#endif /* FXCG50 */
/* img_destroy(): Destroy an image
This function destroys an image. If the image did not own its memory,
nothing happens. Otherwise, the surface memory is released and all
references into it become invalid. */
void img_destroy(img_t img);
//---
// Basic rendering
//---
/* img_fill(): Fill an image with a single color */
void img_fill(img_t img, img_pixel_t color);
/* img_clear(): Fill an image with transparent pixels
Strictly equivalent to img_fill(img, IMG_ALPHA). Just a shortcut. */
void img_clear(img_t img);
/* img_render(): Render an image onto another one
Copies the source image [src] at position (0,0) of target [dst].
-> To copy at position (x,y), replace dst with img_at(dst,x,y).
-> To copy only a section of src, replace src with img_sub(src,x,y,w,h).
Unlike transforms, this function clips the copied rectangle onto the
destination, so the call will work even if src overflows from dst.
Transparent pixels in the source are not copied. If it is necessary to have
transparent pixels of the source become transparent on the destination as
well, first fill the destination with IMG_ALPHA (or use img_clear()).
Note that on fx-CG 50 the VRAM does not support transparency and transparent
pixels will appear as basically black. */
void img_render(img_t src, img_t dst);
/* img_render_vram(): Render an image to the VRAM
This function works like img_render() but renders to VRAM on fx-9860G. This
compensates the lack of img_vram() function on this platform. Rendering to
VRAM supports both monochrome and gray pixels. This function clips the
rendered image onto the VRAM.
On fx-CG 50, this functions is exactly equivalent to:
img_render(src, img_at(img_vram(), x, y)).
@src Source image
@x @y Render destination on VRAM */
void img_render_vram(img_t src, int x, int y);
//---
// Geometric transforms
//
// All geometric transforms render to position (0,0) of the target image and
// fail if the target image is not large enough to hold the transformed result
// (unlike the rendering functions which render only the visible portion).
//
// To render at position (x,y) of the target image, use img_at(). For instance:
// img_hflip(src, img_at(dst, x, y));
//
// Each transform function has a [_create] variant which does the same
// transform but allocates the target image on the fly and returns it. Remember
// that allocation can fail, so you need to check whether the returned image is
// null.
//
// (You can still pass a null image to libimg functions when chaining
// transforms. The null image will be ignored or returned unchanged, so you
// can check for it at the end of any large chain.)
//
// Some functions support in-place transforms. This means they can be called
// with the source as destination, and will transform the image without needing
// new memory. For instance, img_hflip(src, src) flips in-place and replaces
// src with a flipped version of itself.
//
// (However, it is not possible to transform in-place if the source and
// destination intersect in non-trivial ways. The result will be incorrect.)
//
// When transforming to a new image, transparent pixels are ignored, so if the
// destination already has some data, it will not be erased automatically. Use
// img_clear() beforehand to achieve that effect. This allows alpha blending
// while transforming, which is especially useful on the VRAM.
//---
/* img_hflip(): Flip horizontally (supports in-place)
The destination image must have the same size as the source. */
void img_hflip(img_t src, img_t dst);
img_t img_hflip_create(img_t src);
/* img_vflip(): Flip vertically (supports in-place)
The destination image must have the same size as the source. */
void img_vflip(img_t src, img_t dst);
img_t img_vflip_create(img_t src);
/* img_rotate(): Rotate by 90, 180 or 270 degrees (supports in-place)
This function applies a rotation of the specified angle (90, 180 or 270) in
degrees from the source image to the destination image. The rotation can be
performed in-place under the following conditions:
* For 180 degrees: no condition
* For 90 and 270 degrees: width and height of source image are equal
Even if the rotation is not in-place, the target image must have exactly the
size of the rotated source. You can use img_sub() to rotate into part of a
larger destination image. */
void img_rotate(img_t src, img_t dst, int angle);
img_t img_rotate_create(img_t src, int angle);
/* img_upscale(): Upscale by an integer ratio (does not support in-place) */
void img_upscale(img_t src, img_t dst, int scale);
img_t img_upscale_create(img_t src, int scale);
//---
// Color transforms
//
// All color transforms operate on a per-pixel basis. They never change the
// size of the image and can always (trivially) operate in-place. They still
// require the size of the output to be at least the size of the input.
//
// As for geometric transforms, all functions have a [_create] variant that
// copies the image before applying the transform.
//---
/* img_dye(): Replace all non-transparent pixels with a single color
This function repaints all non-transparent pixels of [src] with a fixed
color. This transform is commonly used in games to animate interactions
through various colors (red, white, etc). */
void img_dye(img_t src, img_t dst, img_pixel_t color);
img_t img_dye_create(img_t src, img_pixel_t color);
#ifdef FXCG50
/* img_lighten(): Roughly lighten an image
This function produces a lighter version of the specified image by doubling
the value of each color component (while capping to a maximum to prevent
overflow - this is called saturation arithmetic). Because RGB565 is used,
after 6 iterations, the resulting image will always be white.
This function does not work very well on pixel art but produces convincing
results on artworks and photos. */
void img_lighten(img_t src, img_t dst);
img_t img_lighten_create(img_t src);
/* img_whiten(): Fade an image to white
This function fades the provided image to white by halving the distance of
each color component to its maximum value. The result after 6 iterations
will always be white. */
void img_whiten(img_t src, img_t dst);
img_t img_whiten_create(img_t src);
/* img_darken(): Roughly darken an image (same as fading to black)
This function makes an image darken by halving the value of each color
component. After 6 iterations, the resulting image will always be black.
Note that both img_lighten() and img_darken() destroy some of the
information present in the image. They do not cancel each other. If the
original image is needed at some point, a copy must be made before
lightening or darkening it. */
void img_darken(img_t src, img_t dst);
img_t img_darken_create(img_t src);
#endif /* FXCG50 */
#endif /* LIBIMG_H */

98
src/alloc.c Normal file
View File

@ -0,0 +1,98 @@
#include <libimg.h>
#include "libimg-internal.h"
#include <gint/display.h>
#include <gint/std/stdlib.h>
#include <gint/std/string.h>
int img_target(img_t img, int w, int h)
{
if(img.flags & FLAG_RO) return 0;
if(img_null(img)) return 0;
if(img.width < w || img.height < h) return 0;
return 1;
}
img_t img_create(int w, int h)
{
if(w <= 0 || h <= 0 || w >= 0x10000 || h >= 0x10000)
return NULL_IMG;
img_t s = {
.width = w, .height = h, .stride = w,
.flags = FLAG_OWN,
.pixels = malloc(w * h * sizeof *s.pixels),
};
if(!s.pixels) return NULL_IMG;
return s;
}
img_t img_copy(img_t img)
{
int w = img.width, h = img.height;
img_t s = img_create(w, h);
if(img_null(s)) return s;
img_pixel_t *src_px = img.pixels;
img_pixel_t *dst_px = s.pixels;
for(int y = 0; y < img.height; y++)
{
memcpy(dst_px, src_px, w * sizeof *src_px);
src_px += img.stride;
dst_px += s.stride;
}
return s;
}
int img_null(img_t img)
{
return !img.width || !img.height || !img.pixels;
}
img_t img_sub(img_t img, int x, int y, int w, int h)
{
if(img_null(img)) return img;
/* If w or h is negative, use the full width and height (this formula
works even if x<0 or y<0) */
if(w < 0) w = img.width - x;
if(h < 0) h = img.height - y;
/* Clip in bounds */
if(x < 0) w += x, x = 0;
if(y < 0) h += y, y = 0;
if(x + w > img.width) w = img.width - x;
if(y + h > img.height) h = img.height - y;
if(w <= 0 || h <= 0) return NULL_IMG;
img_t s = {
.width = w, .height = h, .stride = img.stride,
.flags = 0,
.pixels = img.pixels + (y * img.stride) + x,
};
return s;
}
img_t img_at(img_t img, int x, int y)
{
return img_sub(img, x, y, -1, -1);
}
#ifdef FXCG50
img_t img_vram(void)
{
img_t vram = {
.width = 396, .height = 224, .stride = 396,
.flags = 0,
.pixels = gint_vram,
};
return vram;
}
#endif
void img_destroy(img_t img)
{
if(img.flags & FLAG_OWN && !(img.flags & FLAG_RO)) free(img.pixels);
}

90
src/brightness.c Normal file
View File

@ -0,0 +1,90 @@
#include <libimg.h>
#include "libimg-internal.h"
void img_lighten(img_t src, img_t dst)
{
if(!img_target(dst, src.width, src.height)) return;
img_pixel_t *src_px = src.pixels;
img_pixel_t *dst_px = dst.pixels;
for(int y = 0; y < src.height; y++)
{
for(int x = 0; x < src.width; x++)
{
if(src_px[x] == IMG_ALPHA) continue;
int dbl = src_px[x] << 1;
int carry = dbl & 0x10820;
carry = ((carry << 5) - carry) >> 5;
dst_px[x] = carry | (dbl & 0xffff) | 0x0841;
}
src_px += src.stride;
dst_px += dst.stride;
}
}
img_t img_lighten_create(img_t src)
{
img_t dst = img_create(src.width, src.height);
img_fill(dst, IMG_ALPHA);
img_lighten(src, dst);
return dst;
}
void img_whiten(img_t src, img_t dst)
{
if(!img_target(dst, src.width, src.height)) return;
img_pixel_t *src_px = src.pixels;
img_pixel_t *dst_px = dst.pixels;
for(int y = 0; y < src.height; y++)
{
for(int x = 0; x < src.width; x++)
{
if(src_px[x] == IMG_ALPHA) continue;
dst_px[x] = ~((~src_px[x] & 0xf7de) >> 1);
}
src_px += src.stride;
dst_px += dst.stride;
}
}
img_t img_whiten_create(img_t src)
{
img_t dst = img_create(src.width, src.height);
img_fill(dst, IMG_ALPHA);
img_whiten(src, dst);
return dst;
}
void img_darken(img_t src, img_t dst)
{
if(!img_target(dst, src.width, src.height)) return;
img_pixel_t *src_px = src.pixels;
img_pixel_t *dst_px = dst.pixels;
for(int y = 0; y < src.height; y++)
{
for(int x = 0; x < src.width; x++)
{
if(src_px[x] == IMG_ALPHA) continue;
dst_px[x] = (src_px[x] & 0xf7de) >> 1;
}
src_px += src.stride;
dst_px += dst.stride;
}
}
img_t img_darken_create(img_t src)
{
img_t dst = img_create(src.width, src.height);
img_fill(dst, IMG_ALPHA);
img_darken(src, dst);
return dst;
}

30
src/dye.c Normal file
View File

@ -0,0 +1,30 @@
#include <libimg.h>
#include "libimg-internal.h"
void img_dye(img_t src, img_t dst, img_pixel_t color)
{
if(!img_target(dst, src.width, src.height)) return;
img_pixel_t *src_px = src.pixels;
img_pixel_t *dst_px = dst.pixels;
for(int y = 0; y < src.height; y++)
{
for(int x = 0; x < src.width; x++)
{
if(src_px[x] == IMG_ALPHA) continue;
dst_px[x] = color;
}
src_px += src.stride;
dst_px += dst.stride;
}
}
img_t img_dye_create(img_t src, img_pixel_t color)
{
img_t dst = img_create(src.width, src.height);
img_fill(dst, IMG_ALPHA);
img_dye(src, dst, color);
return dst;
}

77
src/flip.c Normal file
View File

@ -0,0 +1,77 @@
#include <libimg.h>
#include "libimg-internal.h"
void img_hflip(img_t src, img_t dst)
{
if(!img_target(dst, src.width, src.height)) return;
img_pixel_t *src_px = src.pixels;
img_pixel_t *dst_px = dst.pixels;
for(int y = 0; y < src.height; y++)
{
for(int x = 0; x < (src.width+1) >> 1; x++)
{
int a = src_px[x];
int b = src_px[src.width-1 - x];
if(b != IMG_ALPHA)
dst_px[x] = b;
if(a != IMG_ALPHA)
dst_px[src.width-1 - x] = a;
}
src_px += src.stride;
dst_px += dst.stride;
}
}
img_t img_hflip_create(img_t src)
{
img_t dst = img_create(src.width, src.height);
img_fill(dst, IMG_ALPHA);
img_hflip(src, dst);
return dst;
}
void img_vflip(img_t src, img_t dst)
{
if(!img_target(dst, src.width, src.height)) return;
int src_fullheight = (src.height - 1) * src.stride;
int dst_fullheight = (src.height - 1) * dst.stride;
img_pixel_t *src_px = src.pixels;
img_pixel_t *dst_px = dst.pixels;
for(int x = 0; x < src.width; x++)
{
int src_offset = 0;
int dst_offset = 0;
for(int y = 0; y < (src.height+1) >> 1; y++)
{
int a = src_px[src_offset];
int b = src_px[src_fullheight - src_offset];
if(b != IMG_ALPHA)
dst_px[dst_offset] = b;
if(a != IMG_ALPHA)
dst_px[dst_fullheight - dst_offset] = a;
src_offset += src.stride;
dst_offset += dst.stride;
}
src_px++;
dst_px++;
}
}
img_t img_vflip_create(img_t src)
{
img_t dst = img_create(src.width, src.height);
img_fill(dst, IMG_ALPHA);
img_hflip(src, dst);
return dst;
}

23
src/libimg-internal.h Normal file
View File

@ -0,0 +1,23 @@
#ifndef LIBIMG_INTERNAL_H
#define LIBIMG_INTERNAL_H
/** Internal img_t flags **/
/* The surface owns its memory and got it through malloc(). Set only on
surfaces allocated with img_create() (and thus, indirectly, img_copy()). */
#define FLAG_OWN 1
/* The surface is read-only. Set only on surfaces generated by conversion from
fxconv, which reference data located in ROM. */
#define FLAG_RO 2
/* Null image */
#define NULL_IMG ((img_t){ 0 })
/* Check whether a surface can be used as a transform target of the specified
size. (Defined in alloc.c.) */
int img_target(img_t img, int w, int h);
/* In-place rotation function */
void img_rotate_inplace(img_t img, int angle);
#endif /* LIBIMG_INTERNAL_H */

68
src/render.c Normal file
View File

@ -0,0 +1,68 @@
#include <libimg.h>
#include "libimg-internal.h"
#include <gint/display.h>
void img_fill(img_t img, img_pixel_t color)
{
img_pixel_t *px = img.pixels;
for(int y = 0; y < img.height; y++, px += img.stride)
for(int x = 0; x < img.width; x++)
{
px[x] = color;
}
}
void img_clear(img_t img)
{
img_fill(img, IMG_ALPHA);
}
void img_render(img_t src, img_t dst)
{
if(img_null(src) || img_null(dst) || dst.flags & FLAG_RO) return;
/* Clip the rectangle */
if(src.width > dst.width)
src = img_sub(src, 0, 0, dst.width, -1);
if(src.height > dst.height)
src = img_sub(src, 0, 0, -1, dst.height);
/* Copy pixels */
img_pixel_t *src_px = src.pixels;
img_pixel_t *dst_px = dst.pixels;
for(int dy = 0; dy < src.height; dy++)
{
for(int dx = 0; dx < src.width; dx++)
{
if(src_px[dx] != IMG_ALPHA)
dst_px[dx] = src_px[dx];
}
src_px += src.stride;
dst_px += dst.stride;
}
}
#ifdef FX9860G
void img_render_vram(img_t src, int x, int y)
{
img_pixel_t *px = img.pixels;
for(int dy = 0; dy < img.height; dy++, px += img.stride)
for(int dx = 0; dx < img.width; dx++)
{
/* TODO: Render using gray? */
dpixel(x+dx, y+dy, px[dx]);
}
}
#endif /* FX9860G */
#ifdef FXCG50
void img_render_vram(img_t img, int x, int y)
{
img_render(img, img_at(img_vram(), x, y));
}
#endif /* FXCG50 */

90
src/rotate-ip.c Normal file
View File

@ -0,0 +1,90 @@
#include <libimg.h>
static void img_rotate_ip90(img_t img)
{
if(img.width != img.height) return;
img_pixel_t *tl = img.pixels;
img_pixel_t *tr = img.pixels + img.width - 1;
img_pixel_t *bl = tl + (img.height - 1) * img.stride;
img_pixel_t *br = tr + (img.height - 1) * img.stride;
for(int y=0; y < img.height >> 1; y++)
{
for(int x=y, dx=0, dy=0; x < img.width-1 - y; x++, dx++,
dy+=img.stride)
{
int swp = tl[dx];
tl[dx] = bl[-dy];
bl[-dy] = br[-dx];
br[-dx] = tr[dy];
tr[dy] = swp;
}
tl += img.stride + 1;
tr += img.stride - 1;
bl -= img.stride - 1;
br -= img.stride + 1;
}
}
static void img_rotate_ip180(img_t img)
{
img_pixel_t *tl = img.pixels;
img_pixel_t *br = tl + (img.height - 1) * img.stride + img.width - 1;
for(int y = 0; y < (img.height+1) >> 1; y++)
{
/* If both pointers reach the same line (height is odd), we
must stop at half of the width */
int limit = img.width;
if(tl+img.stride-1 == br) limit >>= 1;
for(int x = 0; x < limit; x++)
{
int swp = tl[x];
tl[x] = br[-x];
br[-x] = swp;
}
tl += img.stride;
br -= img.stride;
}
}
static void img_rotate_ip270(img_t img)
{
/* Same as img_rotate_ip90, except for the order of assignments */
if(img.width != img.height) return;
img_pixel_t *tl = img.pixels;
img_pixel_t *tr = img.pixels + img.width - 1;
img_pixel_t *bl = tl + (img.height - 1) * img.stride;
img_pixel_t *br = tr + (img.height - 1) * img.stride;
for(int y=0; y < img.height >> 1; y++)
{
for(int x=y, dx=0, dy=0; x < img.width-1 - y; x++, dx++,
dy+=img.stride)
{
/* Only difference with img_rotate_ip90 */
int swp = tl[dx];
tl[dx] = tr[dy];
tr[dy] = br[-dx];
br[-dx] = bl[-dy];
bl[-dy] = swp;
}
tl += img.stride + 1;
tr += img.stride - 1;
bl -= img.stride - 1;
br -= img.stride + 1;
}
}
void img_rotate_inplace(img_t img, int angle)
{
if(angle == 90) img_rotate_ip90(img);
if(angle == 180) img_rotate_ip180(img);
if(angle == 270) img_rotate_ip270(img);
}

86
src/rotate.c Normal file
View File

@ -0,0 +1,86 @@
#include <libimg.h>
#include "libimg-internal.h"
static void img_rotate_90(img_t src, img_t dst)
{
if(!img_target(dst, src.height, src.width)) return;
img_pixel_t *src_px = src.pixels;
img_pixel_t *dst_px = dst.pixels + src.height - 1;
for(int y = 0; y < src.height; y++)
{
for(int x=0, dy=0; x < src.width; x++, dy+=dst.stride)
if(src_px[x] != IMG_ALPHA)
dst_px[dy] = src_px[x];
src_px += src.stride;
dst_px--;
}
}
static void img_rotate_180(img_t src, img_t dst)
{
if(!img_target(dst, src.width, src.height)) return;
img_pixel_t *src_px = src.pixels;
img_pixel_t *dst_px = dst.pixels + (src.height - 1) * dst.stride +
src.width - 1;
for(int y = 0; y < src.height; y++)
{
for(int x=0; x < src.width; x++)
if(src_px[x] != IMG_ALPHA)
dst_px[-x] = src_px[x];
src_px += src.stride;
dst_px -= dst.stride;
}
}
static void img_rotate_270(img_t src, img_t dst)
{
if(!img_target(dst, src.height, src.width)) return;
img_pixel_t *src_px = src.pixels;
img_pixel_t *dst_px = dst.pixels + (src.width - 1) * dst.stride;
for(int y = 0; y < src.height; y++)
{
for(int x=0, dy=0; x < src.width; x++, dy+=dst.stride)
if(src_px[x] != IMG_ALPHA)
dst_px[-dy] = src_px[x];
src_px += src.stride;
dst_px++;
}
}
void img_rotate(img_t src, img_t dst, int angle)
{
if(src.pixels == dst.pixels)
{
img_rotate_inplace(src, angle);
return;
}
if(angle == 0)
{
if(!img_target(dst, src.width, src.height)) return;
img_render(src, dst);
}
if(angle == 90) img_rotate_90(src, dst);
if(angle == 180) img_rotate_180(src, dst);
if(angle == 270) img_rotate_270(src, dst);
}
img_t img_rotate_create(img_t src, int angle)
{
int w = src.width, h = src.height;
if(angle == 90 || angle == 270) w = src.height, h = src.width;
img_t dst = img_create(src.height, src.width);
img_fill(dst, IMG_ALPHA);
img_rotate(src, dst, angle);
return dst;
}

41
src/upscale.c Normal file
View File

@ -0,0 +1,41 @@
#include <libimg.h>
#include "libimg-internal.h"
void img_upscale(img_t src, img_t dst, int scale)
{
if(scale < 1) return;
if(!img_target(dst, scale*src.width, scale*src.height)) return;
if(scale == 1)
{
img_render(src, dst);
return;
}
img_pixel_t *src_px = src.pixels;
img_pixel_t *dst_px = dst.pixels;
for(int y=0, sy=0; y < scale*src.height; y++, sy++)
{
if(sy == scale) src_px += src.stride, sy = 0;
for(int x=0, sx=0; x < scale*src.width; x++, sx++)
{
if(sx == scale) src_px++, sx = 0;
if(*src_px != IMG_ALPHA)
dst_px[x] = *src_px;
}
src_px -= (src.width - 1);
dst_px += dst.stride;
}
}
img_t img_upscale_create(img_t src, int scale)
{
if(scale < 1) return NULL_IMG;
img_t dst = img_create(scale * src.width, scale * src.height);
img_fill(dst, IMG_ALPHA);
img_upscale(src, dst, scale);
return dst;
}

17
testing/Makefile Normal file
View File

@ -0,0 +1,17 @@
src = main.c $(wildcard ../src/*.c)
libimg: $(src)
gcc $^ -I .. -I . -o libimg -D FXCG50
transform.png: transform.bin
convert -size 396x224 rgb565:$< $@
transform.bin: libimg
./libimg
all: libimg transform.png
clean:
@ rm -f libimg transform.bin transform.png
.PHONY: all clean

9
testing/gint/display.h Normal file
View File

@ -0,0 +1,9 @@
#include <stdint.h>
#ifdef FXCG50
extern uint16_t *gint_vram;
#endif
#ifdef FX9860G
extern uint32_t *gint_vram;
#endif

View File

@ -0,0 +1 @@
#include <stdlib.h>

View File

@ -0,0 +1 @@
#include <string.h>

BIN
testing/img/even-odd.bmp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

BIN
testing/img/odd-even.bmp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

BIN
testing/img/sq-even.bmp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

BIN
testing/img/sq-odd.bmp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 KiB

BIN
testing/img/train.bmp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

131
testing/main.c Normal file
View File

@ -0,0 +1,131 @@
#include <libimg.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <stdint.h>
#include <stddef.h>
#include <sys/mman.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <unistd.h>
#include <fcntl.h>
#ifdef FXCG50
uint16_t *gint_vram = NULL;
#endif
#ifdef FX9860G
uint32_t *gint_vram = NULL;
#endif
img_t loadbmp(char const *path)
{
int fd = open(path, O_RDONLY);
if(fd < 0) { printf("%s: %m\n", path); exit(2); }
struct stat statbuf;
fstat(fd, &statbuf);
void *data = mmap(NULL, statbuf.st_size, PROT_READ, MAP_PRIVATE, fd,0);
if(!data) { printf("mmap: %m\n"); exit(3); }
int start = *(uint32_t *)(data + 10);
int width = *(uint32_t *)(data + 18);
int height = *(uint32_t *)(data + 22);
int stride = (width + 1) & ~1;
img_t img = img_create(width, height);
/* Invert rows */
img_pixel_t *src = data + start + 2 * stride * (height - 1);
img_pixel_t *dst = img.pixels;
for(int y = 0; y < height; y++)
{
for(int x = 0; x < width; x++)
{
if(src[x] == IMG_ALPHA) dst[x] = 0x0000;
else if(src[x] == 0xffff) dst[x] = IMG_ALPHA;
else dst[x] = src[x];
}
dst += img.stride;
src -= stride;
}
return img;
}
void saveraw(img_t img, char const *path)
{
int fd = creat(path, 0644);
if(fd < 0) { printf("%s: %m\n", path); exit(4); }
img_pixel_t *px = img.pixels;
for(int y = 0; y < img.height; y++)
{
write(fd, px, img.width*2);
px += img.stride;
}
close(fd);
}
int main(void)
{
img_t sq_even = loadbmp("img/sq-even.bmp");
img_t sq_odd = loadbmp("img/sq-odd.bmp");
img_t odd_even = loadbmp("img/odd-even.bmp");
img_t even_odd = loadbmp("img/even-odd.bmp");
img_t train = loadbmp("img/train.bmp");
img_t in[4] = { sq_even, sq_odd, odd_even, even_odd };
img_t out = img_create(396,224);
img_fill(out, 0xffff);
img_fill(img_sub(out, 138, 0, 28, -1), 0xc618);
for(int i=0, y=8; i<4; i++, y+=52)
{
img_rotate (in[i], img_at(out, 8, y), 90);
img_rotate (in[i], img_at(out, 8, y+26), 0);
img_rotate (in[i], img_at(out, 34, y), 180);
img_rotate (in[i], img_at(out, 34, y+26), 270);
img_upscale(in[i], img_at(out, 60, y+1), 2);
img_hflip (in[i], img_at(out, 110, y));
img_vflip (in[i], img_at(out, 110, y+26));
img_render (in[i], img_at(out, 140, y+13));
img_dye (in[i], img_at(out, 248, y), 0x25ff);
img_dye (in[i], img_at(out, 248, y+26), 0xfd04);
img_t light = img_copy(in[i]);
img_t dark = img_copy(in[i]);
for(int k=0, x=172; k<=2; k++, x+=26)
{
img_whiten(light, light);
img_darken(dark, dark);
img_render(light, img_at(out, x, y));
img_render(dark, img_at(out, x, y+26));
}
img_destroy(light);
img_destroy(dark);
}
img_t light = img_lighten_create(train);
img_t dark = img_darken_create(train);
img_render(light, img_at(out, 282, 24));
img_render(train, img_at(out, 282, 56+24));
img_render(dark, img_at(out, 282, 56+56+24));
saveraw(out, "transform.bin");
img_destroy(out);
img_destroy(sq_odd);
img_destroy(sq_even);
return 0;
}