Discussion:
Running into problems while trying to scale Cairo surface to take up maximum space at specific aspect ratio
(too old to reply)
Blue-Maned_Hawk
2023-06-11 23:13:50 UTC
Permalink
​​Hello!

I'm looking for help with a problem i'm running into with the Cairo
graphics library. I want to paint a surface onto another surface (in
this case an XLib window) in such a way that it will take up as much
space as possible while staying at a 4:3 aspect ratio and be centered
within the window. However, i seemingly can't figure out the right
parameters for cairo_scale(), and am running into issues with a
stretched image and an image that doesn't take up the right amount of space.

Minimum example program demonstrating this behavior below; compile with
`$CC tmp.c -lX11 -lm -lcairo`. Rescale the window and you'll see the
problem i'm facing. (I am fully aware this will lead to a pixelated
output [not visible in the example program where it's just pure white];
this is intentional and exactly what i want to happen.)

#include <X11/Xlib.h>
#include <cairo/cairo.h>
#include <cairo/cairo-xlib.h>
#include <stddef.h>
#include <math.h>

const int width = 640, height = 480;
const long double aspect_ratio = 4.0l/3.0l, inverse_aspect_ratio =
3.0l/4.0l;

int main(void)
{
Display * display = XOpenDisplay(NULL);
int screen = DefaultScreen(display);
Window window = XCreateSimpleWindow(display, RootWindow(display,
screen), 10, 10, width, height, 1, BlackPixel(display, screen),
WhitePixel(display, screen));
XSelectInput(display, window, StructureNotifyMask);
XMapWindow(display, window);

cairo_surface_t * surface = cairo_xlib_surface_create(display,
window, DefaultVisual(display, screen), width, height);
cairo_xlib_surface_set_size(surface, width, height);
cairo_t * root_instance = cairo_create(surface);
cairo_surface_destroy(surface);
surface = cairo_image_surface_create(CAIRO_FORMAT_ARGB32, width,
height);
cairo_t * instance = cairo_create(surface);

cairo_set_source_rgb(instance, 1, 1, 1);
cairo_paint(instance);

for (;;) {
for (XEvent e; XPending(display) != 0; XNextEvent(display, &e))
if (e.type == ConfigureNotify) {
cairo_identity_matrix(root_instance);
if (e.xconfigure.width < width || e.xconfigure.height <
height) {
XResizeWindow(display, window, fmax(width,
e.xconfigure.width), fmax(width, e.xconfigure.height));

cairo_xlib_surface_set_size(cairo_get_target(root_instance), fmax(width,
e.xconfigure.width), fmax(height, e.xconfigure.height));
} else {

cairo_xlib_surface_set_size(cairo_get_target(root_instance),
e.xconfigure.width, e.xconfigure.height);
if (e.xconfigure.width >= aspect_ratio *
e.xconfigure.height) {
cairo_translate(root_instance,
(e.xconfigure.width - e.xconfigure.height * aspect_ratio) / 2, 0);
cairo_scale(root_instance, (e.xconfigure.height
* aspect_ratio) / width, e.xconfigure.height / height);
} else {
cairo_translate(root_instance, 0,
(e.xconfigure.height - e.xconfigure.width * inverse_aspect_ratio) / 2);
cairo_scale(root_instance, e.xconfigure.width /
width, (e.xconfigure.height * inverse_aspect_ratio) / height);
}
}
}

cairo_push_group(root_instance);
cairo_set_source_rgb(root_instance, 0, 0, 0);
cairo_paint(root_instance);
cairo_set_source_surface(root_instance, surface, 0, 0);
cairo_paint(root_instance);
cairo_pop_group_to_source(root_instance);
cairo_paint(root_instance);
cairo_surface_flush(cairo_get_target(root_instance));
}
}

--
⚗︎ | /blu.mɛin.dʰak/ | shortens to "Hawk" | he/him/his/himself/Mr.
bluemanedhawk.github.io
Bitches stole my whole ass ␔🭖᷿᪳𝼗᷍⏧𒒫𐻾ࣛ↉�⃣ quoted-printa
Ben Bacarisse
2023-06-12 00:07:29 UTC
Permalink
Post by Blue-Maned_Hawk
​​Hello!
I'm looking for help with a problem i'm running into with the Cairo
graphics library. I want to paint a surface onto another surface (in this
case an XLib window) in such a way that it will take up as much space as
possible while staying at a 4:3 aspect ratio and be centered within the
window. However, i seemingly can't figure out the right parameters for
cairo_scale(), and am running into issues with a stretched image and an
image that doesn't take up the right amount of space.
Minimum example program demonstrating this behavior below; compile with
`$CC tmp.c -lX11 -lm -lcairo`. Rescale the window and you'll see the
problem i'm facing. (I am fully aware this will lead to a pixelated output
[not visible in the example program where it's just pure white]; this is
intentional and exactly what i want to happen.)
#include <X11/Xlib.h>
#include <cairo/cairo.h>
#include <cairo/cairo-xlib.h>
#include <stddef.h>
#include <math.h>
const int width = 640, height = 480;
const long double aspect_ratio = 4.0l/3.0l, inverse_aspect_ratio =
3.0l/4.0l;
int main(void)
{
Display * display = XOpenDisplay(NULL);
int screen = DefaultScreen(display);
Window window = XCreateSimpleWindow(display, RootWindow(display,
screen), 10, 10, width, height, 1, BlackPixel(display, screen),
WhitePixel(display, screen));
XSelectInput(display, window, StructureNotifyMask);
XMapWindow(display, window);
cairo_surface_t * surface = cairo_xlib_surface_create(display, window,
DefaultVisual(display, screen), width, height);
cairo_xlib_surface_set_size(surface, width, height);
cairo_t * root_instance = cairo_create(surface);
cairo_surface_destroy(surface);
surface = cairo_image_surface_create(CAIRO_FORMAT_ARGB32, width,
height);
cairo_t * instance = cairo_create(surface);
cairo_set_source_rgb(instance, 1, 1, 1);
cairo_paint(instance);
for (;;) {
for (XEvent e; XPending(display) != 0; XNextEvent(display, &e))
This is busy waiting which will ramp up the CPU use. Presumably this is
just for testing, but even so...
Post by Blue-Maned_Hawk
if (e.type == ConfigureNotify) {
cairo_identity_matrix(root_instance);
if (e.xconfigure.width < width || e.xconfigure.height <
height) {
XResizeWindow(display, window, fmax(width,
e.xconfigure.width), fmax(width, e.xconfigure.height));
Surely you wanted fmax(height, e.xconfigure.height) here?
Post by Blue-Maned_Hawk
cairo_xlib_surface_set_size(cairo_get_target(root_instance), fmax(width,
e.xconfigure.width), fmax(height, e.xconfigure.height));
This case looks odd. What's the intent? Do you want stop the user
making the window narrower than width or shorted than height? If that's
what you want you should probably limit e.xconfigure.{width,height}, set
the X window size and then carry on as before because you still need to
set the offset and scale and offset the source in this case as well.
Post by Blue-Maned_Hawk
} else {
cairo_xlib_surface_set_size(cairo_get_target(root_instance),
e.xconfigure.width, e.xconfigure.height);
if (e.xconfigure.width >= aspect_ratio *
e.xconfigure.height) {
cairo_translate(root_instance, (e.xconfigure.width
- e.xconfigure.height * aspect_ratio) / 2, 0);
cairo_scale(root_instance, (e.xconfigure.height *
aspect_ratio) / width, e.xconfigure.height /
height);
I'm pretty sure you did not intend to divide two integers here. Both
e.xconfigure.height and height have integer type.

But there's is a bigger issue. To maintain the aspect ration, the x and y
scaling should be the same. Once you have decided whether it's the
window width or the height that will determine the scaling, you should
calculate the scale factor and use that in both arguments.
Post by Blue-Maned_Hawk
} else {
cairo_translate(root_instance, 0,
(e.xconfigure.height - e.xconfigure.width *
inverse_aspect_ratio) / 2);
cairo_scale(root_instance, e.xconfigure.width /
width, (e.xconfigure.height * inverse_aspect_ratio)
/ height);
}
}
}
cairo_push_group(root_instance);
cairo_set_source_rgb(root_instance, 0, 0, 0);
cairo_paint(root_instance);
cairo_set_source_surface(root_instance, surface, 0, 0);
cairo_paint(root_instance);
cairo_pop_group_to_source(root_instance);
cairo_paint(root_instance);
cairo_surface_flush(cairo_get_target(root_instance));
Do you realise that this is in the outer for (;;) loop? That's a lot of
busy work!
Post by Blue-Maned_Hawk
}
}
--
Ben.
Blue-Maned_Hawk
2023-06-12 00:54:19 UTC
Permalink
<snip/>
Post by Blue-Maned_Hawk
XResizeWindow(display, window, fmax(width,
e.xconfigure.width), fmax(width, e.xconfigure.height));
Surely you wanted fmax(height, e.xconfigure.height) here?
Oops, yes. That was an accident.
Post by Blue-Maned_Hawk
cairo_xlib_surface_set_size(cairo_get_target(root_instance), fmax(width,
e.xconfigure.width), fmax(height, e.xconfigure.height));
This case looks odd. What's the intent? Do you want stop the user
making the window narrower than width or shorted than height?
Yes, i do. I didn't know of any other way to do this.
If that's
what you want you should probably limit e.xconfigure.{width,height}, set
the X window size and then carry on as before because you still need to
set the offset and scale and offset the source in this case as well.
How would i do that?
Post by Blue-Maned_Hawk
} else {
cairo_xlib_surface_set_size(cairo_get_target(root_instance),
e.xconfigure.width, e.xconfigure.height);
if (e.xconfigure.width >= aspect_ratio *
e.xconfigure.height) {
cairo_translate(root_instance, (e.xconfigure.width
- e.xconfigure.height * aspect_ratio) / 2, 0);
cairo_scale(root_instance, (e.xconfigure.height *
aspect_ratio) / width, e.xconfigure.height /
height);
I'm pretty sure you did not intend to divide two integers here. Both
e.xconfigure.height and height have integer type.
But there's is a bigger issue. To maintain the aspect ration, the x and y
scaling should be the same. Once you have decided whether it's the
window width or the height that will determine the scaling, you should
calculate the scale factor and use that in both arguments.
…oh! You're right! It seems like replacing the cairo_scale() calls
with cairo_scale(root_instance, (double)e.xconfigure.height /
(double)height, (double)e.xconfigure.height / (double)height); and the
corresponding call for the width case has worked! Thank you so much!
Post by Blue-Maned_Hawk
cairo_push_group(root_instance);
cairo_set_source_rgb(root_instance, 0, 0, 0);
cairo_paint(root_instance);
cairo_set_source_surface(root_instance, surface, 0, 0);
cairo_paint(root_instance);
cairo_pop_group_to_source(root_instance);
cairo_paint(root_instance);
cairo_surface_flush(cairo_get_target(root_instance));
Do you realise that this is in the outer for (;;) loop? That's a lot of
busy work!
In the actual application, the screen will be getting updated pretty
much every frame, and the loop will be limited to only run once per frame.

--
⚗︎ | /blu.mɛin.dʰak/ | shortens to "Hawk" | he/him/his/himself/Mr.
bluemanedhawk.github.io
Bitches stole my whole ass ␔🭖᷿᪳𝼗᷍⏧𒒫𐻾ࣛ↉�⃣ quoted-printable, can't
have shit in Thunderbir
Ben Bacarisse
2023-06-12 11:24:31 UTC
Permalink
<cut>
Post by Blue-Maned_Hawk
Post by Ben Bacarisse
Post by Blue-Maned_Hawk
cairo_xlib_surface_set_size(cairo_get_target(root_instance), fmax(width,
e.xconfigure.width), fmax(height, e.xconfigure.height));
This case looks odd. What's the intent? Do you want stop the user
making the window narrower than width or shorted than height?
Yes, i do. I didn't know of any other way to do this.
Post by Ben Bacarisse
If that's
what you want you should probably limit e.xconfigure.{width,height}, set
the X window size and then carry on as before because you still need to
set the offset and scale and offset the source in this case as well.
How would i do that?
You could change e.xconfigure.width (and height) but rather than do that
I'd base all the code on two new sizes

int new_width = e.xconfigure.width, new_height = e.xconfigure.height;
if (new_width < width || new_height < height) {
new_width = fmax(width, new_width);
ditto height
set Xwindow size to new_width/new_height
}
what goes here is what was your else clause but using new_width and
new_height rather than the e.xconfigure versions

<cut>
--
Ben.
Ben Bacarisse
2023-06-12 13:48:46 UTC
Permalink
Post by Ben Bacarisse
<cut>
Post by Blue-Maned_Hawk
Post by Ben Bacarisse
Post by Blue-Maned_Hawk
cairo_xlib_surface_set_size(cairo_get_target(root_instance), fmax(width,
e.xconfigure.width), fmax(height, e.xconfigure.height));
This case looks odd. What's the intent? Do you want stop the user
making the window narrower than width or shorted than height?
Yes, i do. I didn't know of any other way to do this.
Post by Ben Bacarisse
If that's
what you want you should probably limit e.xconfigure.{width,height}, set
the X window size and then carry on as before because you still need to
set the offset and scale and offset the source in this case as well.
How would i do that?
You could change e.xconfigure.width (and height) but rather than do that
I'd base all the code on two new sizes
int new_width = e.xconfigure.width, new_height = e.xconfigure.height;
if (new_width < width || new_height < height) {
new_width = fmax(width, new_width);
ditto height
set Xwindow size to new_width/new_height
}
what goes here is what was your else clause but using new_width and
new_height rather than the e.xconfigure versions
Correction. This is a bad idea! The correct way to do this is to rope
in the window manager. Include X11/Xutil.h and then, after creating the
window, do

XSizeHints sizeHints = {
.flags = PMinSize, .min_width = width, .min_height = height
};
XSetWMNormalHints(display, window, &sizeHints);
--
Ben.
Blue-Maned_Hawk
2023-06-12 18:23:52 UTC
Permalink
Post by Ben Bacarisse
Post by Ben Bacarisse
<cut>
Post by Blue-Maned_Hawk
Post by Ben Bacarisse
Post by Blue-Maned_Hawk
cairo_xlib_surface_set_size(cairo_get_target(root_instance), fmax(width,
e.xconfigure.width), fmax(height, e.xconfigure.height));
This case looks odd. What's the intent? Do you want stop the user
making the window narrower than width or shorted than height?
Yes, i do. I didn't know of any other way to do this.
Post by Ben Bacarisse
If that's
what you want you should probably limit e.xconfigure.{width,height}, set
the X window size and then carry on as before because you still need to
set the offset and scale and offset the source in this case as well.
How would i do that?
You could change e.xconfigure.width (and height) but rather than do that
I'd base all the code on two new sizes
int new_width = e.xconfigure.width, new_height = e.xconfigure.height;
if (new_width < width || new_height < height) {
new_width = fmax(width, new_width);
ditto height
set Xwindow size to new_width/new_height
}
what goes here is what was your else clause but using new_width and
new_height rather than the e.xconfigure versions
Correction. This is a bad idea! The correct way to do this is to rope
in the window manager. Include X11/Xutil.h and then, after creating the
window, do
XSizeHints sizeHints = {
.flags = PMinSize, .min_width = width, .min_height = height
};
XSetWMNormalHints(display, window, &sizeHints);
​Thank you! This does exactly what i was trying to do!

--
⚗︎ | /blu.mɛin.dʰak/ | shortens to "Hawk" | he/him/his/himself/Mr.
bluemanedhawk.github.io
Bitches stole my whole ass ␔🭖᷿᪳𝼗᷍⏧𒒫𐻾ࣛ↉�⃣ quoted-printable, can't
have shit

Loading...