Repository: extendr/extendr
Pull request #279 (2021-09-06 22:53:42)

Graphics functions
This PR adds low level graphics functions.

Stretch goal: maybe the ability to define new graphics devices.

The task is made more challenging by the varying support given to each
version of R.

We will likely have to put this behind a feature gate.


andy-thomason: The current challenge is to find some values for `R_GE_gcontext` that actually draws a line!
clauswilke: You'll probably have to set the line type (e.g. 1), line width (e.g. 1), and color (e.g. "blue"). See the documentation and examples for the `gpar()` function, which is mostly a 1-1 mapping to the graphics context.

https://www.rdocumentation.org/packages/grid/versions/3.6.2/topics/gpar
clauswilke: It may also be helpful to look at the grid source code, e.g. the code creating graphics contexts from gpar objects:
https://github.com/wch/r-source/blob/79298c499218846d14500255efd622b5021c10ec/src/library/grid/src/gpar.c#L306
andy-thomason: I think that I'm getting there.

Would anyone like to guess what "cex" stands for. It seems to be a fount size multiplier.
clauswilke: From the documentation of `par()` (in base R):

> `cex` A numerical value giving the amount by which plotting text and symbols should be magnified relative to the default. This starts as 1 when a device is opened, and is reset when the layout is changed, e.g. by setting mfrow.

> Note that some graphics functions such as plot.default have an argument of this name which multiplies this graphical parameter, and some functions such as points and text accept a vector of values which are recycled.

I usually just set it to 1 and keep it there.
andy-thomason: Roadmap for this (deadline 1st October):

* Implement graphics::Context (R_GE_context) - write-only attributes, from_par.
* Implement graphcis::Device - line, polyline, text etc.
* Provide methods to select standard drivers: pdf, postscirpt.
* Test using the postscript device.

Stretch goal:

* Implement a trait for user-defined graphics devices.
* Write a simple SVG renderer.
andy-thomason: We also need to resolve version issues with `#[cfg]s. 

The pattern parameter was added recently causing many of the build failures.

andy-thomason: The postscript driver is working quite well now.

![Screenshot from 2021-09-08 17-55-33](https://user-images.githubusercontent.com/1556316/132552869-39952cf3-8e86-4db3-bb4f-6c7ccb4504af.png)

But the X11 driver is segfaulting when drawing filled regions (circle).

clauswilke: Looks promising! Could we get access to font metrics? Function `GEStrMetric()`.
andy-thomason: Text is next...
andy-thomason: I suspect that the X11 crash is due to the clipping state not being set. I'll try to diagnose this.
clauswilke: > Text is next...

Super, thanks!

Btw., it'll be fairly easy (I think) to write a set of visual tests as part of the integration tests, using vdiffr: https://cran.r-project.org/web/packages/vdiffr/index.html
We just have to write code that generates graphical output, and vdiffr will take care of storing snapshots and comparing for changes.

andy-thomason: The segfaulting issue was because of a version mismatch between the headers and my R library.

I built R from source - fairly easy on unix - and the problem went away.

It may be worth comparing the version in the headers with the version returned by the library.
andy-thomason: ![Screenshot from 2021-09-16 19-57-23](https://user-images.githubusercontent.com/1556316/133669597-0beecbeb-2d7f-4b55-b4cc-57461a2d6ae7.png)

andy-thomason: We should probably call this done for now.

I would be interested to see someone use it for plotting.

We will come back to this later.
Ilia-Kosenkov: I have one comment:
I feel like having all color-related functions at the top level is not the best idea.
Maybe I am spoiled by heavy-oop languages, but  `color::Colors::aliceblue()` looks more appropriate. All color related functions should be under `color` namespace, but predefined colors should reside in their own type or module. 
andy-thomason: Maybe a `predefined` module: `use color::predefined::*`.
Ilia-Kosenkov: It is up to you, I won't oppose leaving all these functions at the top level. But after doing some desktop UI (using another stack), I came to appreciate when there is some `Color` representing just one color, and some `Colors`, members of which are pre-defined, well, *colors*. Sounds easy.

So if I have `struct Color`, I would definitely first look for `fn Colors::white() -> Color` at the same level.
Examples: .NET's [`Color`](https://docs.microsoft.com/en-us/dotnet/api/system.windows.media.color?view=net-5.0) and [`Colors`](https://docs.microsoft.com/en-us/dotnet/api/system.windows.media.colors?view=net-5.0), `R`'s [`colors()`](https://www.rdocumentation.org/packages/grDevices/versions/3.6.2/topics/colors), 



Patch:
diff --git a/extendr-api/build.rs b/extendr-api/build.rs
index a06650e60..e7bbff4f5 100644
--- a/extendr-api/build.rs
+++ b/extendr-api/build.rs
@@ -15,4 +15,8 @@ fn main() {
     if &*major >= "4" && &*minor >= "1" {
         println!("cargo:rustc-cfg=use_r_newenv");
     }
+
+    if &*major >= "4" && &*minor >= "1" {
+        println!("cargo:rustc-cfg=use_r_patternfill");
+    }
 }
diff --git a/extendr-api/src/error.rs b/extendr-api/src/error.rs
index 0fb7b5bd5..1b5477db2 100644
--- a/extendr-api/src/error.rs
+++ b/extendr-api/src/error.rs
@@ -73,6 +73,7 @@ pub enum Error {
     OutOfLimits(Robj),
     TypeMismatch(Robj),
     NamespaceNotFound(Robj),
+    NoGraphicsDevices(Robj),
 
     ExpectedExternalPtrType(Robj, String),
     Other(String),
@@ -148,6 +149,7 @@ impl std::fmt::Display for Error {
             Error::ExpectedExternalPtrType(_robj, type_name) => {
                 write!(f, "Incorrect external pointer type {}", type_name)
             }
+            Error::NoGraphicsDevices(_robj) => write!(f, "No graphics devices active."),
             Error::Other(str) => write!(f, "{}", str),
         }
     }
diff --git a/extendr-api/src/graphics/color.rs b/extendr-api/src/graphics/color.rs
new file mode 100644
index 000000000..41e97e1a8
--- /dev/null
+++ b/extendr-api/src/graphics/color.rs
@@ -0,0 +1,483 @@
+pub struct Color(i32);
+
+impl Color {
+    /// Convert to an R color.
+    pub fn to_i32(&self) -> i32 {
+        self.0
+    }
+
+    /// Generate a color from a CSS-like hex number.
+    /// eg. `Color::hex(0xF0F8FF)`
+    pub fn hex(hex: u32) -> Color {
+        let red = (hex >> 16) & 0xff;
+        let green = (hex >> 8) & 0xff;
+        let blue = hex & 0xff;
+        Color(red as i32 | (green as i32) << 8 | (blue as i32) << 16 | 0xff << 24)
+    }
+
+    /// Generate a color from a 3 digit CSS-like hex number.
+    /// eg. `Color::hex(0xF0F)`
+    pub fn hex3(hex: u32) -> Color {
+        let red = ((hex >> 8) & 0xf) * 0xff / 0x0f;
+        let green = ((hex >> 4) & 0xf) * 0xff / 0x0f;
+        let blue = (hex & 0xf) * 0xff / 0x0f;
+        Color(red as i32 | (green as i32) << 8 | (blue as i32) << 16 | 0xff << 24)
+    }
+
+    /// Generate a color from rgb components (0-255).
+    pub fn rgb(red: u8, green: u8, blue: u8) -> Color {
+        Color(red as i32 | (green as i32) << 8 | (blue as i32) << 16 | 0xff << 24)
+    }
+
+    /// Generate a color from rgba components (0-255).
+    pub fn rgba(red: u8, green: u8, blue: u8, alpha: u8) -> Color {
+        Color(red as i32 | (green as i32) << 8 | (blue as i32) << 16 | (alpha as i32) << 24)
+    }
+}
+
+pub mod predefined {
+    use super::Color;
+
+    /// https://www.w3.org/TR/2018/REC-css-color-3-20180619/
+    pub fn aliceblue() -> Color {
+        Color::hex(0xF0F8FF)
+    }
+    pub fn antiquewhite() -> Color {
+        Color::hex(0xFAEBD7)
+    }
+    pub fn aqua() -> Color {
+        Color::hex(0x00FFFF)
+    }
+    pub fn aquamarine() -> Color {
+        Color::hex(0x7FFFD4)
+    }
+    pub fn azure() -> Color {
+        Color::hex(0xF0FFFF)
+    }
+    pub fn beige() -> Color {
+        Color::hex(0xF5F5DC)
+    }
+    pub fn bisque() -> Color {
+        Color::hex(0xFFE4C4)
+    }
+    pub fn black() -> Color {
+        Color::hex(0x000000)
+    }
+    pub fn blanchedalmond() -> Color {
+        Color::hex(0xFFEBCD)
+    }
+    pub fn blue() -> Color {
+        Color::hex(0x0000FF)
+    }
+    pub fn blueviolet() -> Color {
+        Color::hex(0x8A2BE2)
+    }
+    pub fn brown() -> Color {
+        Color::hex(0xA52A2A)
+    }
+    pub fn burlywood() -> Color {
+        Color::hex(0xDEB887)
+    }
+    pub fn cadetblue() -> Color {
+        Color::hex(0x5F9EA0)
+    }
+    pub fn chartreuse() -> Color {
+        Color::hex(0x7FFF00)
+    }
+    pub fn chocolate() -> Color {
+        Color::hex(0xD2691E)
+    }
+    pub fn coral() -> Color {
+        Color::hex(0xFF7F50)
+    }
+    pub fn cornflowerblue() -> Color {
+        Color::hex(0x6495ED)
+    }
+    pub fn cornsilk() -> Color {
+        Color::hex(0xFFF8DC)
+    }
+    pub fn crimson() -> Color {
+        Color::hex(0xDC143C)
+    }
+    pub fn cyan() -> Color {
+        Color::hex(0x00FFFF)
+    }
+    pub fn darkblue() -> Color {
+        Color::hex(0x00008B)
+    }
+    pub fn darkcyan() -> Color {
+        Color::hex(0x008B8B)
+    }
+    pub fn darkgoldenrod() -> Color {
+        Color::hex(0xB8860B)
+    }
+    pub fn darkgray() -> Color {
+        Color::hex(0xA9A9A9)
+    }
+    pub fn darkgreen() -> Color {
+        Color::hex(0x006400)
+    }
+    pub fn darkgrey() -> Color {
+        Color::hex(0xA9A9A9)
+    }
+    pub fn darkkhaki() -> Color {
+        Color::hex(0xBDB76B)
+    }
+    pub fn darkmagenta() -> Color {
+        Color::hex(0x8B008B)
+    }
+    pub fn darkolivegreen() -> Color {
+        Color::hex(0x556B2F)
+    }
+    pub fn darkorange() -> Color {
+        Color::hex(0xFF8C00)
+    }
+    pub fn darkorchid() -> Color {
+        Color::hex(0x9932CC)
+    }
+    pub fn darkred() -> Color {
+        Color::hex(0x8B0000)
+    }
+    pub fn darksalmon() -> Color {
+        Color::hex(0xE9967A)
+    }
+    pub fn darkseagreen() -> Color {
+        Color::hex(0x8FBC8F)
+    }
+    pub fn darkslateblue() -> Color {
+        Color::hex(0x483D8B)
+    }
+    pub fn darkslategray() -> Color {
+        Color::hex(0x2F4F4F)
+    }
+    pub fn darkslategrey() -> Color {
+        Color::hex(0x2F4F4F)
+    }
+    pub fn darkturquoise() -> Color {
+        Color::hex(0x00CED1)
+    }
+    pub fn darkviolet() -> Color {
+        Color::hex(0x9400D3)
+    }
+    pub fn deeppink() -> Color {
+        Color::hex(0xFF1493)
+    }
+    pub fn deepskyblue() -> Color {
+        Color::hex(0x00BFFF)
+    }
+    pub fn dimgray() -> Color {
+        Color::hex(0x696969)
+    }
+    pub fn dimgrey() -> Color {
+        Color::hex(0x696969)
+    }
+    pub fn dodgerblue() -> Color {
+        Color::hex(0x1E90FF)
+    }
+    pub fn firebrick() -> Color {
+        Color::hex(0xB22222)
+    }
+    pub fn floralwhite() -> Color {
+        Color::hex(0xFFFAF0)
+    }
+    pub fn forestgreen() -> Color {
+        Color::hex(0x228B22)
+    }
+    pub fn fuchsia() -> Color {
+        Color::hex(0xFF00FF)
+    }
+    pub fn gainsboro() -> Color {
+        Color::hex(0xDCDCDC)
+    }
+    pub fn ghostwhite() -> Color {
+        Color::hex(0xF8F8FF)
+    }
+    pub fn gold() -> Color {
+        Color::hex(0xFFD700)
+    }
+    pub fn goldenrod() -> Color {
+        Color::hex(0xDAA520)
+    }
+    pub fn gray() -> Color {
+        Color::hex(0x808080)
+    }
+    pub fn green() -> Color {
+        Color::hex(0x008000)
+    }
+    pub fn greenyellow() -> Color {
+        Color::hex(0xADFF2F)
+    }
+    pub fn grey() -> Color {
+        Color::hex(0x808080)
+    }
+    pub fn honeydew() -> Color {
+        Color::hex(0xF0FFF0)
+    }
+    pub fn hotpink() -> Color {
+        Color::hex(0xFF69B4)
+    }
+    pub fn indianred() -> Color {
+        Color::hex(0xCD5C5C)
+    }
+    pub fn indigo() -> Color {
+        Color::hex(0x4B0082)
+    }
+    pub fn ivory() -> Color {
+        Color::hex(0xFFFFF0)
+    }
+    pub fn khaki() -> Color {
+        Color::hex(0xF0E68C)
+    }
+    pub fn lavender() -> Color {
+        Color::hex(0xE6E6FA)
+    }
+    pub fn lavenderblush() -> Color {
+        Color::hex(0xFFF0F5)
+    }
+    pub fn lawngreen() -> Color {
+        Color::hex(0x7CFC00)
+    }
+    pub fn lemonchiffon() -> Color {
+        Color::hex(0xFFFACD)
+    }
+    pub fn lightblue() -> Color {
+        Color::hex(0xADD8E6)
+    }
+    pub fn lightcoral() -> Color {
+        Color::hex(0xF08080)
+    }
+    pub fn lightcyan() -> Color {
+        Color::hex(0xE0FFFF)
+    }
+    pub fn lightgoldenrodyellow() -> Color {
+        Color::hex(0xFAFAD2)
+    }
+    pub fn lightgray() -> Color {
+        Color::hex(0xD3D3D3)
+    }
+    pub fn lightgreen() -> Color {
+        Color::hex(0x90EE90)
+    }
+    pub fn lightgrey() -> Color {
+        Color::hex(0xD3D3D3)
+    }
+    pub fn lightpink() -> Color {
+        Color::hex(0xFFB6C1)
+    }
+    pub fn lightsalmon() -> Color {
+        Color::hex(0xFFA07A)
+    }
+    pub fn lightseagreen() -> Color {
+        Color::hex(0x20B2AA)
+    }
+    pub fn lightskyblue() -> Color {
+        Color::hex(0x87CEFA)
+    }
+    pub fn lightslategray() -> Color {
+        Color::hex(0x778899)
+    }
+    pub fn lightslategrey() -> Color {
+        Color::hex(0x778899)
+    }
+    pub fn lightsteelblue() -> Color {
+        Color::hex(0xB0C4DE)
+    }
+    pub fn lightyellow() -> Color {
+        Color::hex(0xFFFFE0)
+    }
+    pub fn lime() -> Color {
+        Color::hex(0x00FF00)
+    }
+    pub fn limegreen() -> Color {
+        Color::hex(0x32CD32)
+    }
+    pub fn linen() -> Color {
+        Color::hex(0xFAF0E6)
+    }
+    pub fn magenta() -> Color {
+        Color::hex(0xFF00FF)
+    }
+    pub fn maroon() -> Color {
+        Color::hex(0x800000)
+    }
+    pub fn mediumaquamarine() -> Color {
+        Color::hex(0x66CDAA)
+    }
+    pub fn mediumblue() -> Color {
+        Color::hex(0x0000CD)
+    }
+    pub fn mediumorchid() -> Color {
+        Color::hex(0xBA55D3)
+    }
+    pub fn mediumpurple() -> Color {
+        Color::hex(0x9370DB)
+    }
+    pub fn mediumseagreen() -> Color {
+        Color::hex(0x3CB371)
+    }
+    pub fn mediumslateblue() -> Color {
+        Color::hex(0x7B68EE)
+    }
+    pub fn mediumspringgreen() -> Color {
+        Color::hex(0x00FA9A)
+    }
+    pub fn mediumturquoise() -> Color {
+        Color::hex(0x48D1CC)
+    }
+    pub fn mediumvioletred() -> Color {
+        Color::hex(0xC71585)
+    }
+    pub fn midnightblue() -> Color {
+        Color::hex(0x191970)
+    }
+    pub fn mintcream() -> Color {
+        Color::hex(0xF5FFFA)
+    }
+    pub fn mistyrose() -> Color {
+        Color::hex(0xFFE4E1)
+    }
+    pub fn moccasin() -> Color {
+        Color::hex(0xFFE4B5)
+    }
+    pub fn navajowhite() -> Color {
+        Color::hex(0xFFDEAD)
+    }
+    pub fn navy() -> Color {
+        Color::hex(0x000080)
+    }
+    pub fn oldlace() -> Color {
+        Color::hex(0xFDF5E6)
+    }
+    pub fn olive() -> Color {
+        Color::hex(0x808000)
+    }
+    pub fn olivedrab() -> Color {
+        Color::hex(0x6B8E23)
+    }
+    pub fn orange() -> Color {
+        Color::hex(0xFFA500)
+    }
+    pub fn orangered() -> Color {
+        Color::hex(0xFF4500)
+    }
+    pub fn orchid() -> Color {
+        Color::hex(0xDA70D6)
+    }
+    pub fn palegoldenrod() -> Color {
+        Color::hex(0xEEE8AA)
+    }
+    pub fn palegreen() -> Color {
+        Color::hex(0x98FB98)
+    }
+    pub fn paleturquoise() -> Color {
+        Color::hex(0xAFEEEE)
+    }
+    pub fn palevioletred() -> Color {
+        Color::hex(0xDB7093)
+    }
+    pub fn papayawhip() -> Color {
+        Color::hex(0xFFEFD5)
+    }
+    pub fn peachpuff() -> Color {
+        Color::hex(0xFFDAB9)
+    }
+    pub fn peru() -> Color {
+        Color::hex(0xCD853F)
+    }
+    pub fn pink() -> Color {
+        Color::hex(0xFFC0CB)
+    }
+    pub fn plum() -> Color {
+        Color::hex(0xDDA0DD)
+    }
+    pub fn powderblue() -> Color {
+        Color::hex(0xB0E0E6)
+    }
+    pub fn purple() -> Color {
+        Color::hex(0x800080)
+    }
+    pub fn red() -> Color {
+        Color::hex(0xFF0000)
+    }
+    pub fn rosybrown() -> Color {
+        Color::hex(0xBC8F8F)
+    }
+    pub fn royalblue() -> Color {
+        Color::hex(0x4169E1)
+    }
+    pub fn saddlebrown() -> Color {
+        Color::hex(0x8B4513)
+    }
+    pub fn salmon() -> Color {
+        Color::hex(0xFA8072)
+    }
+    pub fn sandybrown() -> Color {
+        Color::hex(0xF4A460)
+    }
+    pub fn seagreen() -> Color {
+        Color::hex(0x2E8B57)
+    }
+    pub fn seashell() -> Color {
+        Color::hex(0xFFF5EE)
+    }
+    pub fn sienna() -> Color {
+        Color::hex(0xA0522D)
+    }
+    pub fn silver() -> Color {
+        Color::hex(0xC0C0C0)
+    }
+    pub fn skyblue() -> Color {
+        Color::hex(0x87CEEB)
+    }
+    pub fn slateblue() -> Color {
+        Color::hex(0x6A5ACD)
+    }
+    pub fn slategray() -> Color {
+        Color::hex(0x708090)
+    }
+    pub fn slategrey() -> Color {
+        Color::hex(0x708090)
+    }
+    pub fn snow() -> Color {
+        Color::hex(0xFFFAFA)
+    }
+    pub fn springgreen() -> Color {
+        Color::hex(0x00FF7F)
+    }
+    pub fn steelblue() -> Color {
+        Color::hex(0x4682B4)
+    }
+    pub fn tan() -> Color {
+        Color::hex(0xD2B48C)
+    }
+    pub fn teal() -> Color {
+        Color::hex(0x008080)
+    }
+    pub fn thistle() -> Color {
+        Color::hex(0xD8BFD8)
+    }
+    pub fn tomato() -> Color {
+        Color::hex(0xFF6347)
+    }
+    pub fn turquoise() -> Color {
+        Color::hex(0x40E0D0)
+    }
+    pub fn violet() -> Color {
+        Color::hex(0xEE82EE)
+    }
+    pub fn wheat() -> Color {
+        Color::hex(0xF5DEB3)
+    }
+    pub fn white() -> Color {
+        Color::hex(0xFFFFFF)
+    }
+    pub fn whitesmoke() -> Color {
+        Color::hex(0xF5F5F5)
+    }
+    pub fn yellow() -> Color {
+        Color::hex(0xFFFF00)
+    }
+    pub fn yellowgreen() -> Color {
+        Color::hex(0x9ACD32)
+    }
+}
diff --git a/extendr-api/src/graphics/mod.rs b/extendr-api/src/graphics/mod.rs
new file mode 100644
index 000000000..f880e962f
--- /dev/null
+++ b/extendr-api/src/graphics/mod.rs
@@ -0,0 +1,775 @@
+use crate::*;
+use libR_sys::*;
+
+pub mod color;
+
+use color::Color;
+
+pub struct Context {
+    context: R_GE_gcontext,
+    xscale: (f64, f64),
+    yscale: (f64, f64),
+    offset: (f64, f64),
+    scalar: f64,
+}
+
+#[derive(Clone, Debug, PartialEq)]
+pub struct Device {
+    inner: pGEDevDesc,
+}
+
+#[derive(Clone, Debug, PartialEq)]
+pub struct Pattern {
+    inner: Robj,
+}
+
+#[derive(Clone, Debug, PartialEq)]
+pub struct TextMetric {
+    ascent: f64,
+    descent: f64,
+    width: f64,
+}
+
+#[derive(Clone, Debug, PartialEq)]
+pub struct Raster<P: AsRef<[u32]>> {
+    pixels: P,
+    width: usize,
+}
+
+impl Device {
+    pub(crate) fn inner(&self) -> pGEDevDesc {
+        self.inner
+    }
+
+    // pub(crate) fn asref(&self) -> &GEDevDesc {
+    //     unsafe { &*self.inner }
+    // }
+
+    // pub(crate) fn dev(&self) -> &DevDesc {
+    //     unsafe { &*self.asref().dev }
+    // }
+}
+
+#[derive(PartialEq, Debug, Clone)]
+pub enum LineEnd {
+    RoundCap,
+    ButtCap,
+    SquareCap,
+}
+
+#[derive(PartialEq, Debug, Clone)]
+pub enum LineJoin {
+    RoundJoin,
+    MitreJoin,
+    BevelJoin,
+}
+
+#[derive(PartialEq, Debug, Clone)]
+pub enum LineType {
+    Blank,
+    Solid,
+    Dashed,
+    Dotted,
+    DotDash,
+    LongDash,
+    TwoDash,
+}
+
+#[derive(PartialEq, Debug, Clone)]
+pub enum Unit {
+    Device,
+    Normalized,
+    Inches,
+    CM,
+}
+
+#[derive(PartialEq, Debug, Clone)]
+pub enum FontFace {
+    PlainFont,
+    BoldFont,
+    ItalicFont,
+    BoldItalicFont,
+    SymbolFont,
+}
+
+fn unit_to_ge(unit: Unit) -> GEUnit {
+    match unit {
+        Unit::Device => GEUnit_GE_DEVICE,
+        Unit::Normalized => GEUnit_GE_NDC,
+        Unit::Inches => GEUnit_GE_INCHES,
+        Unit::CM => GEUnit_GE_CM,
+    }
+}
+
+impl Context {
+    pub fn from_device(dev: &Device, unit: Unit) -> Self {
+        #[allow(unused_unsafe)]
+        unsafe {
+            let offset = dev.to_device_coords((0., 0.), unit.clone());
+            let mut xscale = dev.to_device_coords((1., 0.), unit.clone());
+            let mut yscale = dev.to_device_coords((0., 1.), unit);
+            xscale.0 -= offset.0;
+            xscale.1 -= offset.1;
+            yscale.0 -= offset.0;
+            yscale.1 -= offset.1;
+
+            // sqrt(abs(det(m)))
+            let scalar = (xscale.0 * yscale.1 - xscale.1 * yscale.0).abs().sqrt();
+
+            let mut context = R_GE_gcontext {
+                col: Color::rgb(0xff, 0xff, 0xff).to_i32(),
+                fill: Color::rgb(0xc0, 0xc0, 0xc0).to_i32(),
+                gamma: 1.0,
+                lwd: 1.0,
+                lty: 0,
+                lend: R_GE_lineend_GE_ROUND_CAP,
+                ljoin: R_GE_linejoin_GE_ROUND_JOIN,
+                lmitre: 10.0,
+                cex: 1.0,
+                ps: 14.0,
+                lineheight: 1.0,
+                fontface: 1,
+                fontfamily: [0; 201],
+
+                #[cfg(use_r_patternfill)]
+                patternFill: R_NilValue,
+            };
+
+            context
+                .fontfamily
+                .iter_mut()
+                .zip(b"Helvetica".iter())
+                .for_each(|(d, s)| *d = *s as i8);
+
+            Self {
+                context,
+                xscale,
+                yscale,
+                offset,
+                scalar,
+            }
+        }
+    }
+
+    /// Set the line or text color of a primitive.
+    pub fn color(&mut self, col: Color) -> &mut Self {
+        self.context.col = col.to_i32();
+        self
+    }
+
+    /// Set the fill color of a primitive.
+    pub fn fill(&mut self, fill: Color) -> &mut Self {
+        self.context.fill = fill.to_i32();
+        self
+    }
+
+    /// Set the gamma of the device. `out_color = in_color ** gamma`
+    pub fn gamma(&mut self, gamma: f64) -> &mut Self {
+        self.context.gamma = gamma;
+        self
+    }
+
+    /// Set the width of the line in chosen units.
+    pub fn line_width(&mut self, lwd: f64) -> &mut Self {
+        self.context.lwd = (lwd * self.scalar).max(1.0);
+        self
+    }
+
+    /// Set the type of the line.
+    /// ```ignore
+    /// Blank    => <invisible>
+    /// Solid    => ------
+    /// Dashed   => - - - -
+    /// Dotted   => . . . .
+    /// DotDash  => . - . -
+    /// LongDash => --  --
+    /// TwoDash  => . . - -
+    /// ```
+    pub fn line_type(&mut self, lty: LineType) -> &mut Self {
+        use LineType::*;
+        self.context.lty = match lty {
+            Blank => -1,
+            Solid => 0,
+            Dashed => 4 + (4 << 4),
+            Dotted => 1 + (3 << 4),
+            DotDash => 1 + (3 << 4) + (4 << 8) + (3 << 12),
+            LongDash => 7 + (3 << 4),
+            TwoDash => 2 + (2 << 4) + (6 << 8) + (2 << 12),
+        };
+        self
+    }
+
+    /// Set the line end type.
+    /// ```ignore
+    ///   LineEnd::RoundCap
+    ///   LineEnd::ButtCap  
+    ///   LineEnd::SquareCap
+    /// ```
+    pub fn line_end(&mut self, lend: LineEnd) -> &mut Self {
+        self.context.lend = match lend {
+            LineEnd::RoundCap => 1,
+            LineEnd::ButtCap => 2,
+            LineEnd::SquareCap => 3,
+        };
+        self
+    }
+
+    /// Set the line join type.
+    /// ```ignore
+    ///   LineJoin::RoundJoin
+    ///   LineJoin::MitreJoin
+    ///   LineJoin::BevelJoin
+    /// ```
+    pub fn line_join(&mut self, ljoin: LineJoin) -> &mut Self {
+        self.context.ljoin = match ljoin {
+            LineJoin::RoundJoin => 1,
+            LineJoin::MitreJoin => 2,
+            LineJoin::BevelJoin => 3,
+        };
+        self
+    }
+
+    pub fn point_size(&mut self, ps: f64) -> &mut Self {
+        self.context.ps = ps;
+        self
+    }
+
+    /// Set the line miter limit - the point where the line becomes a bevel join.
+    pub fn line_mitre(&mut self, lmitre: f64) -> &mut Self {
+        self.context.lmitre = lmitre * self.scalar;
+        self
+    }
+
+    /// Set the line height for text.
+    pub fn line_height(&mut self, lineheight: f64) -> &mut Self {
+        self.context.lineheight = lineheight;
+        self
+    }
+
+    // pub fn char_extra_size(&mut self, cex: f64) -> &mut Self {
+    //     self.context.cex = cex;
+    //     self
+    // }
+
+    /// Set the font face.
+    /// ```ignore
+    ///   FontFace::PlainFont
+    ///   FontFace::BoldFont
+    ///   FontFace::ItalicFont
+    ///   FontFace::BoldItalicFont
+    ///   FontFace::SymbolFont
+    /// ```
+    pub fn font_face(&mut self, fontface: FontFace) -> &mut Self {
+        use FontFace::*;
+        self.context.fontface = match fontface {
+            PlainFont => 1,
+            BoldFont => 2,
+            ItalicFont => 3,
+            BoldItalicFont => 4,
+            SymbolFont => 5,
+        };
+        self
+    }
+
+    //
+    pub fn font_family(&mut self, fontfamily: &str) -> &mut Self {
+        let maxlen = self.context.fontfamily.len() - 1;
+
+        for c in self.context.fontfamily.iter_mut() {
+            *c = 0;
+        }
+
+        for (i, b) in fontfamily.bytes().enumerate().take(maxlen) {
+            self.context.fontfamily[i] = b as std::os::raw::c_char;
+        }
+        self
+    }
+
+    /// Set the transform as a 3x2 matrix.
+    pub fn transform(
+        &mut self,
+        xscale: (f64, f64),
+        yscale: (f64, f64),
+        offset: (f64, f64),
+    ) -> &mut Self {
+        self.xscale = xscale;
+        self.yscale = yscale;
+        self.offset = offset;
+        self
+    }
+
+    pub(crate) fn context(&self) -> pGEcontext {
+        unsafe { std::mem::transmute(&self.context) }
+    }
+
+    // Affine transform.
+    pub(crate) fn t(&self, xy: (f64, f64)) -> (f64, f64) {
+        (
+            self.offset.0 + xy.0 * self.xscale.0 + xy.1 * self.yscale.0,
+            self.offset.1 + xy.0 * self.xscale.1 + xy.1 * self.yscale.1,
+        )
+    }
+
+    // Affine relative transform (width, height).
+    pub(crate) fn trel(&self, wh: (f64, f64)) -> (f64, f64) {
+        (
+            wh.0 * self.xscale.0 + wh.1 * self.yscale.0,
+            wh.0 * self.xscale.1 + wh.1 * self.yscale.1,
+        )
+    }
+
+    // Scalar transform (eg. radius etc).
+    pub(crate) fn ts(&self, value: f64) -> f64 {
+        value * self.scalar
+    }
+
+    // Inverse scalar transform (eg. text width etc).
+    pub(crate) fn its(&self, value: f64) -> f64 {
+        value / self.scalar
+    }
+
+    pub(crate) fn tmetric(&self, tm: TextMetric) -> TextMetric {
+        TextMetric {
+            ascent: tm.ascent / self.scalar,
+            descent: tm.descent / self.scalar,
+            width: tm.width / self.scalar,
+        }
+    }
+}
+
+#[allow(non_snake_case)]
+impl Device {
+    /// Get the current device.
+    pub fn current() -> Result<Device> {
+        // At present we can't trap an R error from a function
+        // that does not return a SEXP.
+        unsafe {
+            Ok(Device {
+                inner: GEcurrentDevice(),
+            })
+        }
+    }
+
+    /// Enable device rendering.
+    pub fn mode_on(&self) -> Result<()> {
+        unsafe {
+            if Rf_NoDevices() != 0 {
+                Err(Error::NoGraphicsDevices(r!(())))
+            } else {
+                GEMode(1, self.inner());
+                Ok(())
+            }
+        }
+    }
+
+    /// Disable device rendering and flush.
+    pub fn mode_off(&self) -> Result<()> {
+        unsafe {
+            if Rf_NoDevices() != 0 {
+                Err(Error::NoGraphicsDevices(r!(())))
+            } else {
+                GEMode(0, self.inner());
+                Ok(())
+            }
+        }
+    }
+
+    /// Get the device number for this device.
+    pub fn device_number(&self) -> i32 {
+        unsafe { GEdeviceNumber(self.inner()) }
+    }
+
+    /// Get a device by number.
+    pub fn get_device(number: i32) -> Result<Device> {
+        unsafe {
+            if number < 0 || number >= Rf_NumDevices() {
+                Err(Error::NoGraphicsDevices(r!(())))
+            } else {
+                Ok(Device {
+                    inner: GEgetDevice(number),
+                })
+            }
+        }
+    }
+
+    /// Convert device coordinates into a specified unit.
+    /// This is usually done by the API.
+    pub fn from_device_coords(&self, value: (f64, f64), from: Unit) -> (f64, f64) {
+        let from = unit_to_ge(from);
+        unsafe {
+            (
+                GEfromDeviceX(value.0, from, self.inner()),
+                GEfromDeviceY(value.1, from, self.inner()),
+            )
+        }
+    }
+
+    /// Convert a specified unit coordinates into device coordinates.
+    /// This is usually done by the API.
+    pub fn to_device_coords(&self, value: (f64, f64), to: Unit) -> (f64, f64) {
+        if to == Unit::Device {
+            value
+        } else {
+            let to = unit_to_ge(to);
+            unsafe {
+                (
+                    GEtoDeviceX(value.0, to, self.inner()),
+                    GEtoDeviceY(value.1, to, self.inner()),
+                )
+            }
+        }
+    }
+
+    /// Convert device width/height coordinates into a specified unit.
+    /// This is usually done by the API.
+    pub fn from_device_wh(&self, value: (f64, f64), from: Unit) -> (f64, f64) {
+        let from = unit_to_ge(from);
+        unsafe {
+            (
+                GEfromDeviceWidth(value.0, from, self.inner()),
+                GEfromDeviceHeight(value.1, from, self.inner()),
+            )
+        }
+    }
+
+    /// Convert a specified unit width/height coordinates into device coordinates.
+    /// This is usually done by the API.
+    pub fn to_device_wh(&self, value: (f64, f64), to: Unit) -> (f64, f64) {
+        let to = unit_to_ge(to);
+        unsafe {
+            (
+                GEtoDeviceWidth(value.0, to, self.inner()),
+                GEtoDeviceHeight(value.1, to, self.inner()),
+            )
+        }
+    }
+
+    /// Start a new page. The page color can be set in advance.
+    pub fn new_page(&self, gc: &Context) {
+        unsafe { GENewPage(gc.context(), self.inner()) }
+    }
+
+    /// Change the clip rectangle.
+    pub fn clip(&self, from: (f64, f64), to: (f64, f64), gc: &Context) {
+        let from = gc.t(from);
+        let to = gc.t(to);
+        unsafe { GESetClip(from.0, from.1, to.0, to.1, self.inner()) }
+    }
+
+    /// Draw a stroked line. gc.color() is the stroke color.
+    pub fn line(&self, from: (f64, f64), to: (f64, f64), gc: &Context) {
+        let from = gc.t(from);
+        let to = gc.t(to);
+        unsafe { GELine(from.0, from.1, to.0, to.1, gc.context(), self.inner()) }
+    }
+
+    /// Draw a stroked/filled polyline. gc.color() is the stroke color.
+    /// The input is anything yielding (x,y) coordinate pairs.
+    /// Polylines are not closed.
+    pub fn polyline<T: IntoIterator<Item = (f64, f64)>>(&self, coords: T, gc: &Context) {
+        let (mut x, mut y): (Vec<_>, Vec<_>) = coords.into_iter().map(|xy| gc.t(xy)).unzip();
+        let xptr = x.as_mut_slice().as_mut_ptr();
+        let yptr = y.as_mut_slice().as_mut_ptr();
+        unsafe {
+            GEPolyline(
+                x.len() as std::os::raw::c_int,
+                xptr,
+                yptr,
+                gc.context(),
+                self.inner(),
+            )
+        }
+    }
+
+    /// Draw a stroked/filled polygon. gc.color() is the stroke color.
+    /// The input is anything yielding (x,y) coordinate pairs.
+    /// Polygons are closed.
+    pub fn polygon<T: IntoIterator<Item = (f64, f64)>>(&self, coords: T, gc: &Context) {
+        let (mut x, mut y): (Vec<_>, Vec<_>) = coords.into_iter().map(|xy| gc.t(xy)).unzip();
+        let xptr = x.as_mut_slice().as_mut_ptr();
+        let yptr = y.as_mut_slice().as_mut_ptr();
+        unsafe {
+            GEPolygon(
+                x.len() as std::os::raw::c_int,
+                xptr,
+                yptr,
+                gc.context(),
+                self.inner(),
+            )
+        }
+    }
+
+    // /// Return a list of (x, y) points generated from a spline.
+    // /// The iterator returns ((x, y), s) where s is -1 to 1.
+    // pub fn xspline<T: Iterator<Item = ((f64, f64), f64)> + Clone>(
+    //     &self,
+    //     coords: T,
+    //     open: bool,
+    //     rep_ends: bool,
+    //     draw: bool,
+    //     gc: &Context,
+    // ) -> Robj {
+    //     let (mut x, mut y): (Vec<_>, Vec<_>) = coords
+    //         .clone()
+    //         .map(|(xy, _s)| gc.t(xy))
+    //         .unzip();
+    //     let mut s: Vec<_> = coords.map(|(_xy, s)| s).collect();
+    //     let xptr = x.as_mut_slice().as_mut_ptr();
+    //     let yptr = y.as_mut_slice().as_mut_ptr();
+    //     let sptr = s.as_mut_slice().as_mut_ptr();
+    //     unsafe {
+    //         new_owned(GEXspline(
+    //             x.len() as std::os::raw::c_int,
+    //             xptr,
+    //             yptr,
+    //             sptr,
+    //             if open { 1 } else { 0 },
+    //             if rep_ends { 1 } else { 0 },
+    //             if draw { 1 } else { 0 },
+    //             gc.context(),
+    //             self.inner(),
+    //         ))
+    //     }
+    // }
+
+    /// Draw a stroked/filled circle.
+    /// gc.color() is the stroke color.
+    /// gc.fill() is the fill color.
+    pub fn circle(&self, center: (f64, f64), radius: f64, gc: &Context) {
+        let center = gc.t(center);
+        let radius = gc.ts(radius);
+        unsafe { GECircle(center.0, center.1, radius, gc.context(), self.inner()) }
+    }
+
+    /// Draw a stroked/filled axis-aligned rectangle.
+    /// gc.color() is the stroke color.
+    /// gc.fill() is the fill color.
+    pub fn rectangle(&self, from: (f64, f64), to: (f64, f64), gc: &Context) {
+        let from = gc.t(from);
+        let to = gc.t(to);
+        unsafe { GELine(from.0, from.1, to.0, to.1, gc.context(), self.inner()) }
+    }
+
+    /// Draw a path with multiple segments.
+    /// gc.color() is the stroke color.
+    /// gc.fill() is the fill color.
+    /// The input is an interator of iterators yielding (x,y) pairs.
+    pub fn path<T: IntoIterator<Item = impl IntoIterator<Item = (f64, f64)>>>(
+        &self,
+        coords: T,
+        winding: bool,
+        gc: &Context,
+    ) {
+        let mut x = Vec::new();
+        let mut y = Vec::new();
+        let mut nper: Vec<std::os::raw::c_int> = Vec::new();
+        let coords = coords.into_iter();
+        for segment in coords {
+            let mut n = 0;
+            for xy in segment {
+                let xy = gc.t(xy);
+                x.push(xy.0);
+                y.push(xy.1);
+                n += 1;
+            }
+            nper.push(n);
+        }
+
+        let xptr = x.as_mut_slice().as_mut_ptr();
+        let yptr = y.as_mut_slice().as_mut_ptr();
+        let nperptr = nper.as_mut_slice().as_mut_ptr();
+        unsafe {
+            GEPath(
+                xptr,
+                yptr,
+                nper.len() as std::os::raw::c_int,
+                nperptr,
+                if winding { 1 } else { 0 },
+                gc.context(),
+                self.inner(),
+            )
+        }
+    }
+
+    /// Screen capture. Returns an integer matrix representing pixels if it is able.
+    pub fn capture(&self) -> Robj {
+        unsafe { new_owned(GECap(self.inner())) }
+    }
+
+    /// Draw a bitmap.
+    pub fn raster<T: AsRef<[u32]>>(
+        &self,
+        raster: Raster<T>,
+        pos: (f64, f64),
+        size: (f64, f64),
+        angle: f64,
+        interpolate: bool,
+        gc: &Context,
+    ) {
+        let (x, y) = gc.t(pos);
+        let (width, height) = gc.trel(size);
+        let w = raster.width;
+        let pixels = raster.pixels.as_ref();
+        let h = pixels.len() / w;
+        unsafe {
+            let raster = pixels.as_ptr() as *mut u32;
+            let w = w as i32;
+            let h = h as i32;
+            let interpolate = if interpolate { 1 } else { 0 };
+            GERaster(
+                raster,
+                w,
+                h,
+                x,
+                y,
+                width,
+                height,
+                angle,
+                interpolate,
+                gc.context(),
+                self.inner(),
+            )
+        };
+    }
+
+    /// Draw a text string starting at pos.
+    /// TODO: do we need to convert units?
+    pub fn text<T: AsRef<str>>(
+        &self,
+        pos: (f64, f64),
+        text: T,
+        center: (f64, f64),
+        rot: f64,
+        gc: &Context,
+    ) {
+        unsafe {
+            let (x, y) = gc.t(pos);
+            let (xc, yc) = gc.trel(center);
+            let text = std::ffi::CString::new(text.as_ref()).unwrap();
+            let enc = cetype_t_CE_UTF8;
+            GEText(
+                x,
+                y,
+                text.as_ptr(),
+                enc,
+                xc,
+                yc,
+                rot,
+                gc.context(),
+                self.inner(),
+            );
+        }
+    }
+
+    /// Draw a special symbol centered on pos.
+    /// See https://stat.ethz.ch/R-manual/R-devel/library/graphics/html/points.html
+    pub fn symbol(&self, pos: (f64, f64), symbol: i32, size: f64, gc: &Context) {
+        unsafe {
+            let (x, y) = gc.t(pos);
+            GESymbol(x, y, symbol, gc.ts(size), gc.context(), self.inner());
+        }
+    }
+
+    /// Get the metrics for a single unicode codepoint.
+    pub fn char_metric(&self, c: char, gc: &Context) -> TextMetric {
+        unsafe {
+            let mut res = TextMetric {
+                ascent: 0.0,
+                descent: 0.0,
+                width: 0.0,
+            };
+            GEMetricInfo(
+                c as i32,
+                gc.context(),
+                &mut res.ascent as *mut f64,
+                &mut res.descent as *mut f64,
+                &mut res.width as *mut f64,
+                self.inner(),
+            );
+            gc.tmetric(res)
+        }
+    }
+
+    /// Get the width of a unicode string.
+    pub fn text_width<T: AsRef<str>>(&self, text: T, gc: &Context) -> f64 {
+        let text = std::ffi::CString::new(text.as_ref()).unwrap();
+        let enc = cetype_t_CE_UTF8;
+        unsafe { gc.its(GEStrWidth(text.as_ptr(), enc, gc.context(), self.inner())) }
+    }
+
+    /// Get the height of a unicode string.
+    pub fn text_height<T: AsRef<str>>(&self, text: T, gc: &Context) -> f64 {
+        let text = std::ffi::CString::new(text.as_ref()).unwrap();
+        let enc = cetype_t_CE_UTF8;
+        unsafe { gc.its(GEStrHeight(text.as_ptr(), enc, gc.context(), self.inner())) }
+    }
+
+    /// Get the metrics for a unicode string.
+    pub fn text_metric<T: AsRef<str>>(&self, text: T, gc: &Context) -> TextMetric {
+        let text = std::ffi::CString::new(text.as_ref()).unwrap();
+        let enc = cetype_t_CE_UTF8;
+        unsafe {
+            let mut res = TextMetric {
+                ascent: 0.0,
+                descent: 0.0,
+                width: 0.0,
+            };
+            GEStrMetric(
+                text.as_ptr(),
+                enc,
+                gc.context(),
+                &mut res.ascent as *mut f64,
+                &mut res.descent as *mut f64,
+                &mut res.width as *mut f64,
+                self.inner(),
+            );
+            gc.tmetric(res)
+        }
+    }
+
+    /// Get the width of a mathematical expression.
+    pub fn math_text_width(&self, expr: &Robj, gc: &Context) -> f64 {
+        unsafe { gc.its(GEExpressionWidth(expr.get(), gc.context(), self.inner())) }
+    }
+
+    /// Get the height of a mathematical expression.
+    pub fn math_text_height(&self, expr: &Robj, gc: &Context) -> f64 {
+        unsafe { gc.its(GEExpressionHeight(expr.get(), gc.context(), self.inner())) }
+    }
+
+    /// Get the metrics for a mathematical expression.
+    pub fn math_text_metric(&self, expr: &Robj, gc: &Context) -> TextMetric {
+        unsafe {
+            let mut res = TextMetric {
+                ascent: 0.0,
+                descent: 0.0,
+                width: 0.0,
+            };
+            GEExpressionMetric(
+                expr.get(),
+                gc.context(),
+                &mut res.ascent as *mut f64,
+                &mut res.descent as *mut f64,
+                &mut res.width as *mut f64,
+                self.inner(),
+            );
+            gc.tmetric(res)
+        }
+    }
+
+    /// Draw a mathematical expression.
+    pub fn math_text(
+        &self,
+        expr: &Robj,
+        pos: (f64, f64),
+        center: (f64, f64),
+        rot: f64,
+        gc: &Context,
+    ) {
+        unsafe {
+            let (x, y) = gc.t(pos);
+            let (xc, yc) = gc.trel(center);
+            GEMathText(x, y, expr.get(), xc, yc, rot, gc.context(), self.inner());
+        }
+    }
+}
diff --git a/extendr-api/src/lib.rs b/extendr-api/src/lib.rs
index 2b1efa969..6d49346fe 100644
--- a/extendr-api/src/lib.rs
+++ b/extendr-api/src/lib.rs
@@ -232,6 +232,7 @@ pub mod ownership;
 pub mod prelude;
 pub mod rmacros;
 
+pub mod graphics;
 pub mod robj;
 pub mod scalar;
 pub mod thread_safety;


Test patch:
diff --git a/extendr-api/tests/graphics_tests.rs b/extendr-api/tests/graphics_tests.rs
new file mode 100644
index 000000000..cf301aca6
--- /dev/null
+++ b/extendr-api/tests/graphics_tests.rs
@@ -0,0 +1,94 @@
+use extendr_api::graphics::color::predefined::{antiquewhite, black, darkkhaki, deepskyblue};
+use extendr_api::graphics::color::Color;
+use extendr_api::graphics::{Context, Device, FontFace, Unit};
+use extendr_api::prelude::*;
+
+#[test]
+fn graphics_test() {
+    // Set this to false to render with your local graphics device.
+    let use_postscript = true;
+    let dir = std::env::temp_dir();
+    let path = dir.join("test.ps");
+    let path_str = path.to_string_lossy().to_string();
+    test! {
+        if use_postscript {
+            R!("postscript({{path_str}})")?;
+        }
+        let dev = Device::current()?;
+        let mut gc = Context::from_device(&dev, Unit::Inches);
+
+        // Start a new page.
+        // Use CSS "antiquewhite" https://www.w3.org/TR/2018/REC-css-color-3-20180619/
+        gc.fill(antiquewhite());
+        dev.new_page(&gc);
+
+        dev.mode_on()?;
+
+        // Graphics commands.
+        gc.color(darkkhaki());
+        gc.line_width(0.1);
+
+        // Draw a line.
+        dev.line((2.0, 2.0), (3.0, 3.0), &gc);
+
+        // Draw a circle using `polygon()`.
+        let scale = std::f64::consts::PI*2.0/10.0;
+        gc.fill(deepskyblue());
+        dev.polygon(
+            (0..10).map(|i| (
+                ((i as f64) * scale).cos() + 4.0,
+                ((i as f64) * scale).sin() + 2.0
+            )), &gc);
+
+        // Draw a circle using `circle()`.
+        gc.fill(Color::rgb(0x20, 0x20, 0xc0));
+        dev.circle((1.0, 1.0), 0.5, &gc);
+
+        gc.color(black());
+        gc.point_size(36.0);
+        gc.font_face(FontFace::PlainFont);
+        gc.font_family("Helvetica");
+
+        // Draw Hello -- World with the two dashes almost touching.
+        let w = dev.text_width("Hello -", &gc);
+        dev.text((1.0, 3.0), "Hello -", (0.0, 0.0), 0.0, &gc);
+        dev.text((1.0 + w, 3.0), "- World", (0.0, 0.0), 0.0, &gc);
+
+        gc.line_width(0.01);
+        for i in 0..10 {
+            dev.symbol((1.0 + i as f64 * 0.3, 4.0), i, 0.25, &gc);
+        }
+
+        println!("{:?}", dev.char_metric('a', &gc));
+        println!("{:?}", dev.char_metric('g', &gc));
+        println!("{:?}", dev.char_metric('ê', &gc));
+
+        println!("{:?}", dev.text_metric("a", &gc));
+        println!("{:?}", dev.text_metric("g", &gc));
+        println!("{:?}", dev.text_metric("ê", &gc));
+
+        dev.mode_off()?;
+
+        if use_postscript {
+            // Flush the file and kill the device.
+            R!("dev.off()")?;
+            let ps = std::fs::read_to_string(path).expect("PS file not written.");
+            if let Some(split) = ps.split_once("%%EndProlog") {
+                let epilogue = split.1;
+                println!("epilogue:\n{}", epilogue);
+
+                // Graphics commands.
+                // Note windows version uses \r\n, hence the newline at the start.
+                // assert!(epilogue.contains("\no"));
+                // assert!(epilogue.contains("\ncp p3"));
+                // assert!(epilogue.contains("\nc p3"));
+            } else {
+                println!("ps:\n{}", ps);
+                assert!(("epilogue not found", false).1);
+            }
+        } else {
+            std::thread::sleep(std::time::Duration::from_millis(2000));
+            assert!(false); // did we check in the flag as "false?"
+        }
+    }
+}