Skip to content
Prev 61070 / 63421 Next

Question about grid.group compositing operators in cairo

Hi

Thanks for the code (and for the previous attachments).

Some thoughts so far (HTML version with images attached) ...

<1>
As you have pointed out, the Cairo device draws a stroked-and-filled 
shape with two separate drawing operations:  the path is filled and then 
the path is stroked.  I do not believe that there is any alternative in 
Cairo graphics (apart from filling and stroking as an isolated group and 
then drawing the group, which we will come back to).

<2>
This fill-then-stroke approach is easy to demonstrate just with a thick
semitransparent border ...

library(grid)
overlapRect <- rectGrob(width=.5, height=.5,
                         gp=gpar(fill="green", lwd=20,
                                 col=rgb(1,0,0,.5)))
grid.newpage()
grid.draw(overlapRect)

<3>
This fill-then-stroke approach is what happens on many (most?) graphics 
devices, including, for example, the core windows() device, the core 
quartz() device, the 'ragg' devices, and 'ggiraph'.  The latter is true 
because this is actually the defined behaviour for SVG ...

https://www.w3.org/TR/SVG2/render.html#Elements
https://www.w3.org/TR/SVG2/render.html#PaintingShapesAndText

<4>
There are exceptions to the fill-then-stroke approach, including the 
core pdf() device, but I think they are in the minority.  The PDF 
language supports a "B" operator that only fills within the border (no 
overlap between fill and border).  Demonstrating this is complicated by 
the fact that not all PDF viewers support this correctly (e.g., evince 
and Firefox do not;  ghostscript and chrome do)!

<5>
Forcing all R graphics devices to change the rendering of 
filled-and-stroked shapes to match the PDF definition instead of 
fill-then-stroke is unlikely to happen because it would impact a lot of 
graphics devices, it would break existing behaviour, it may be 
difficult/impossible for some devices, and it is not clear that it is 
the best approach anyway.

<6>
Finally getting back to your example, the fill-then-stroke approach 
produces some interesting results when applying compositing operators 
because the fill is drawn using the compositing operator to combine it 
with previous drawing and then the stroke is drawn using the compositing 
operator to combine it with *the result of combining the fill with 
previous drawing*.  The result makes sense in terms of how the rendering 
works, but it probably fails the "principle of least surprise".

srcRect <- rectGrob(2/3, 1/3, width=.6, height=.6,
                     gp=gpar(lwd = 5, fill=rgb(0, 0, 0.9, 0.4)))
dstRect <- rectGrob(1/3, 2/3, width=.6, height=.6,
                     gp=gpar(lwd = 5, fill=rgb(0.7, 0, 0, 0.8)))
grid.newpage()
grid.group(srcRect, "in", dstRect)

<7>
This issue is not entirely unanticipated because it can arise 
slightly-less-unintentionally if we combine a 'src' and/or 'dst' that 
draw more than one shape, like this ...

src <- circleGrob(3:4/5, r=.2, gp=gpar(col=NA, fill=2))
dst <- circleGrob(1:2/5, r=.2, gp=gpar(col=NA, fill=3))
grid.newpage()
grid.group(src, "xor", dst)

This was discussed in the Section "Compositing and blend modes" in the 
original technical report about groups and compositing ...

https://www.stat.auckland.ac.nz/~paul/Reports/GraphicsEngine/groups/groups.html#userdetails

<8>
A solution to the problem of drawing more than one shape (above) is to 
take explicit control of how shapes are combined, *using explicit 
groups* ...

grid.newpage()
grid.group(groupGrob(src), "xor", dst)

<9>
Explicit groups can be used to solve the problem of overlapping fill and 
stroke (here we specify that the rectangle border should be combined 
with the rectangle fill using the "source" operator) ...

grid.newpage()
grid.group(overlapRect, "source")

<10>
Explicit groups can also be used to get the result that we might have 
originally expected for the "in" operator example (here we isolate the 
'src' rectangle so that the border and the fill are combined together 
[using the default "over" operator] and then combined with the other 
rectangle using the "in" operator) ...

grid.newpage()
grid.group(groupGrob(srcRect), "in", dstRect)

<11>
A possible change would be to force an implicit group (with op=OVER) on 
the 'src' and 'dst' in dev->group().  I believe this is effectively what 
you are doing with your dsvg() device (?).

Currently, dev->group() does this ...

[OVER] shape shape shape OP shape shape shape

... and an implicit group on 'src' and 'dst' would do this ...

([OVER] shape shape shape) OP ([OVER] shape shape shape)

An implicit (OVER) group would make it easier to combine multiple shapes 
with OVER (though only slightly) ...

grid.group(src, OP, dst)

... instead of ...

grid.group(groupGrob(src), OP, dst)

On the other hand, an implicit (OVER) group would make it harder to 
combine multiple shapes with an operator other than OVER (by quite a 
lot?) ...

grid.group(groupGrob(shape, OP, groupGrob(shape, OP, shape)), OP, dst)

... instead of ...

grid.group(src, OP, dst)

The complicating factor is that what constitutes more than one shape (or 
drawing operation) can be unexpected ...

gList(rectGrob(), rectGrob())  ## obvious
rectGrob(width=1:2/2)          ## less obvious
rectGrob(gp=gpar(col=, fill=)) ## a bit of a surprise

<12>
In summary, while there is some temptation to add an implicit group 
around 'src' and 'dst' in a group, there are also reasons not to.

Happy to hear further arguments on this.

Paul
On 28/09/2022 8:04 am, Panagiotis Skintzos wrote: