cairo_surface_set_device_scale(): backend inconsistency and double-scaling risk
0 reputation · 22 Feb 2022, 23:39 UTC
0 reputation · 22 Feb 2022, 23:39 UTC
A migration to Cairo 1.18.0 for HiDPI support involves using cairo_surface_set_device_scale() on image surfaces. The objective is to decouple the logical coordinate system from the native pixel buffer to maintain sharpness on high-density displays.
While the API adjusts the logical pixel ratio, it does not resize the underlying pixel buffer. A significant compatibility boundary exists because backends such as PDF, PostScript, and X11 ignore the device scale entirely, leading to inconsistent output across different surface types. Furthermore, if a cairo_t context already possesses a scaling transform—common in GTK-managed environments—applying a surface device scale may result in double-scaling, causing blurred or pixelated rendering.
35275 reputation · 23 Feb 2022, 08:46 UTC
There is no introspection API that tells you whether a backend honors device scale — cairo_surface_get_device_scale() only returns the value you set, not what the backend does with it. The practical answer to all three open questions is the same: treat the cairo_t CTM as the single authoritative scaling factor, keep the surface device scale at 1.0, and use cairo_surface_get_type() only to drive diagnostics and tests. This works uniformly because the CTM is respected by every backend, while device scale is not.
Confirmed (well-established Cairo behavior, valid for 1.18.0):
cairo_surface_set_device_scale() maps logical coordinates to device pixels but never resizes the underlying buffer. You must allocate the image surface at width × scale by height × scale yourself.Likely explanation for your inconsistency: you are mixing two scaling mechanisms. When a GTK-managed cairo_t already carries a scale via its CTM (GTK applies the widget scale factor internally), adding a surface device scale multiplies them — effective scale = CTM × device scale — which produces the blurring or oversized output you describe.
cairo_surface_set_device_scale() on a surface you draw to through a GTK-provided context. GTK has already scaled the CTM; adding surface scale is the double-scaling bug.cairo_scale(cr, s, s). Prefer the context approach — it survives a backend swap (e.g., offscreen image reused as PDF fallback) without code changes.Since no capability query exists, classify by surface type and assume the conservative case:
switch (cairo_surface_get_type(surface)) {
case CAIRO_SURFACE_TYPE_IMAGE:
/* device scale honored */
break;
case CAIRO_SURFACE_TYPE_PDF:
case CAIRO_SURFACE_TYPE_PS:
case CAIRO_SURFACE_TYPE_SVG:
case CAIRO_SURFACE_TYPE_XLIB:
case CAIRO_SURFACE_TYPE_XCB:
/* device scale ignored: scale via cairo_scale() instead */
break;
default:
/* platform-dependent: verify empirically */
break;
}
Write one minimal test per backend: draw a 100×100 logical rectangle with device scale 2.0 and no context scale, then measure output. Image surface should yield 200×200 device pixels; PDF/PS/SVG output will be 100×100. Also confirm your runtime version with cairo_version() against CAIRO_VERSION_ENCODE(1,18,0), and in GTK compare gtk_widget_get_scale_factor() with the context CTM at draw time.
One caution: cairo_push_group() creates an intermediate image surface that inherits the parent's device scale, so nested groups can compound scales silently — another reason to keep device scale at 1.0 everywhere.
Use comments to ask for clarification. Post a solution as an answer.
No question comments on this page.