[Rd] Question about grid.group compositing operators in cairo

Paul Murrell p@u| @end|ng |rom @t@t@@uck|@nd@@c@nz
Wed Oct 12 23:27:57 CEST 2022


Hi

This issue has expanded to include the behaviour of compositing 
operators in R graphics more generally.

For the record, the discussion is continuing here ...

https://github.com/pmur002/rgraphics-compositing

Paul

On 4/10/22 09:20, Paul Murrell wrote:
> 
> Interim update:  I have spoken with Thomas Lin Pedersen (cc'ed), the 
> author/maintainer of 'ragg' and 'svglite', who is working on adding 
> group support for those graphics devices and he has voted in support of 
> the current Cairo implementation, so the needle has shifted towards 
> Cairo at this stage.
> 
> I still want to do more tests on other devices to gather more evidence.
> 
> Paul
> 
> p.s.  Attached (if it makes it through the filters) is a manual 
> modification of your original dsvg() example that has been changed so 
> that it produces the Cairo result.  This is probably not exactly how you 
> would want to implement the dsvg() solution, but it is at least a proof 
> of concept that the Cairo result can be produced in SVG.
> 
> On 30/09/22 10:49, Paul Murrell wrote:
>> Hi
>>
>> Some more thoughts ...
>>
>> <1>
>> I said before that currently, dev->group() does this ...
>>
>> [OVER] shape shape shape OP shape shape shape
>>
>> ... and one option would be an implicit group on 'src' and 'dst' like 
>> this ...
>>
>> ([OVER] shape shape shape) OP ([OVER] shape shape shape)
>>
>> ... but another approach could be just an implicit group on each 
>> shape, like this ...
>>
>> [OVER] ([OVER] shape) ([OVER] shape) OP ([OVER] shape) ([OVER] shape)
>>
>> That may be a better representation of what you are already doing with 
>> dsvg() ?  It may also better reflect what naturally occurs in some 
>> graphics systems.
>>
>> <2>
>> Changing the Cairo implementation to work like that would I think 
>> produce the same result as your dsvg() for ...
>>
>> grid.group(src, "in", dst)
>>
>> ... and it would make what constitutes more than one shape much less 
>> surprising ...
>>
>> gList(rectGrob(), rectGrob())  ## multiple shapes (obviously)
>> rectGrob(width=1:2/2)          ## multiple shapes (less obvious)
>> rectGrob(gp=gpar(col=, fill=)) ## NOT multiple shapes (no surprise)
>>
>> ... and it should not break any pre-existing non-group behaviour.
>>
>> <3>
>> One casualty from this third option would be that the following would 
>> no longer solve the overlapping fill and stroke problem ...
>>
>> grid.group(overlapRect, "source")
>>
>> ... although the fact that that currently works is really a bit 
>> surprising AND that result could still be achieved by explicitly 
>> drawing separate shapes ...
>>
>> grid.group(rectGrob(gp=gpar(col=rgb(1,0,0,.5), lwd=20, fill=NA)),
>>             "source",
>>             rectGrob(gp=gpar(col=NA, fill="green")))
>>
>> <4>
>> I need to try some of this out and also check in with some other 
>> people who I think are working on implementing groups on different 
>> graphics devices.
>>
>> <5>
>> In summary, don't go changing dsvg() too much just yet!
>>
>> Paul
>>
>> On 29/09/2022 1:30 pm, Paul Murrell wrote:
>>> Hi
>>>
>>> Would it work to explicitly record a filled-and-stroked shape as two 
>>> separate elements (one only filled and one only stroked) ?
>>>
>>> Then it should only be as hard to apply the active operator on both 
>>> of those elements as it is to apply the active operator to more than 
>>> one shape (?)
>>>
>>> Paul
>>>
>>> On 29/09/22 10:17, Panagiotis Skintzos wrote:
>>>> Thank you for the very thorough explanation Paul.
>>>>
>>>> To answer your question on 11: The dsvg device, simply defines svg
>>>> elements with their attributes (rect with fill & stroke in my 
>>>> examples).
>>>> It does not do any internal image processing like cairo.
>>>>
>>>> My concern is how to proceed with the implementation in dsvg.
>>>>
>>>> If I leave it as it is now, they're will be cases where it will give
>>>> different results from cairo (and perhaps other devices that will
>>>> implement group compositing in similar way).
>>>>
>>>> On the other hand It would be quite challenging in practice to simulate
>>>> the cairo implementation and apply first the fill and then the stroke
>>>> with the active operator, on the element itself.
>>>>
>>>> Any suggestions? :-)
>>>>
>>>> Panagiotis
>>>>
>>>>
>>>> On 28/9/22 02:56, Paul Murrell wrote:
>>>>  > 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#Elements>
>>>>  > https://www.w3.org/TR/SVG2/render.html#PaintingShapesAndText 
>>>> <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 <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:
>>>>  >> Here is the code again in text:
>>>>  >>
>>>>  >>
>>>>  >> src <- rectGrob(2/3, 1/3, width=.6, height=.6, gp=gpar(lwd = 5,
>>>>  >> fill=rgb(0, 0, 0.9, 0.4)))
>>>>  >> dst <- rectGrob(1/3, 2/3, width=.6, height=.6, gp=gpar(lwd = 5,
>>>>  >> fill=rgb(0.7, 0, 0, 0.8)))
>>>>  >>
>>>>  >> svg("cairo.in.svg", width = 5, height = 5)
>>>>  >> grid.group(src, "in", dst)
>>>>  >> dev.off()
>>>>  >>
>>>>  >>
>>>>  >>
>>>>  >> On 27/9/22 04:44, Paul Murrell wrote:
>>>>  >>  >
>>>>  >>  > Could you also please send me the SVG code that your device is
>>>>  >>  > generating for your example.  Thanks!
>>>>  >>  >
>>>>  >>  > Paul
>>>>  >>  >
>>>>  >>  > On 27/09/22 08:50, Paul Murrell wrote:
>>>>  >>  >> Hi
>>>>  >>  >>
>>>>  >>  >> Thanks for the report.  It certainly sounds like I have done
>>>>  >>  >> something stupid :)  For my debugging and testing could you 
>>>> please
>>>>  >>  >> share the R code from your tests ?  Thanks!
>>>>  >>  >>
>>>>  >>  >> Paul
>>>>  >>  >>
>>>>  >>  >> On 26/09/22 10:27, Panagiotis Skintzos wrote:
>>>>  >>  >>> Hello,
>>>>  >>  >>>
>>>>  >>  >>> I'm trying to update ggiraph package in graphic engine v15
>>>>  >>  >>> (currently we support up to v14).
>>>>  >>  >>>
>>>>  >>  >>> I've implemented the group operators and when I compare the 
>>>> outputs
>>>>  >>  >>> of ggiraph::dsvg with the outputs of svg/png, I noticed 
>>>> some weird
>>>>  >>  >>> results.
>>>>  >>  >>>
>>>>  >>  >>> Specifically, some operators in cairo (in, out, dest.in, 
>>>> dest.atop)
>>>>  >>  >>> give strange output, when any source element in the group 
>>>> has a
>>>>  >>  >>> stroke color defined.
>>>>  >>  >>>
>>>>  >>  >>> I attach three example images, where two stroked rectangles 
>>>> are
>>>>  >> used
>>>>  >>  >>> as source (right) and destination (left).
>>>>  >>  >>>
>>>>  >>  >>> cairo.over.png shows the result of the over operator in cairo
>>>>  >>  >>>
>>>>  >>  >>> cairo.in.png shows the result of the in operator in cairo
>>>>  >>  >>>
>>>>  >>  >>> dsvg.in.png shows the result of the in operator in dsvg
>>>>  >>  >>>
>>>>  >>  >>>
>>>>  >>  >>> You can see the difference between cairo.in.png and 
>>>> dsvg.in.png. I
>>>>  >>  >>> found out why I get different results:
>>>>  >>  >>>
>>>>  >>  >>> In dsvg implementation there is one drawing operation: Draw 
>>>> the
>>>>  >>  >>> source element, as whole (fill and stroke) over the 
>>>> destination
>>>>  >>  >>> element (using feComposite filter)
>>>>  >>  >>>
>>>>  >>  >>> In cairo implementation though there are two operations: 
>>>> Apply the
>>>>  >>  >>> fill on source and draw over the destination and then apply 
>>>> the
>>>>  >>  >>> stroke and draw over the result of the previous operation.
>>>>  >>  >>>
>>>>  >>  >>> I'm not sure if this is intentional or not. Shouldn't the 
>>>> source
>>>>  >>  >>> element being drawn first as whole (fill and stroke with over
>>>>  >>  >>> operator) and then apply the group operator and draw it 
>>>> over the
>>>>  >>  >>> destination? It would seem more logical that way.
>>>>  >>  >>>
>>>>  >>  >>>
>>>>  >>  >>> Thanks,
>>>>  >>  >>>
>>>>  >>  >>> Panagiotis
>>>>  >>  >>>
>>>>  >>  >>>
>>>>  >>  >>> ______________________________________________
>>>>  >>  >>> R-devel using r-project.org mailing list
>>>>  >>  >>> https://stat.ethz.ch/mailman/listinfo/r-devel 
>>>> <https://stat.ethz.ch/mailman/listinfo/r-devel>
>>>>  >> <https://stat.ethz.ch/mailman/listinfo/r-devel 
>>>> <https://stat.ethz.ch/mailman/listinfo/r-devel>>
>>>>  >>  >>
>>>>  >>  >
>>>>  >
>>>
>>
> 

-- 
Dr Paul Murrell
Te Kura Tatauranga | Department of Statistics
Waipapa Taumata Rau | The University of Auckland
Private Bag 92019, Auckland 1142, New Zealand
64 9 3737599 x85392
paul using stat.auckland.ac.nz
www.stat.auckland.ac.nz/~paul/



More information about the R-devel mailing list