During the implementation of PDF soft masks in our WPF version of PDFRasterizer.NET we realized that there is little information about WPF opacity masks on the internet. The only information that exists is about applying masks declaratively (in XAML) but it is unclear how to use them imperatively.
I assume that you are familiar with the concept of WPF opacity masks, otherwise you can read about it here.
There are two approaches how an opacity mask can be applied to an instance of the Visual class. The first approach is to assign an opacity mask to a drawing visual instance. This approach has several restrictions that reduce its flexibility. First of all, the mask is applied only when the Close() method of the DrawingContext is called. Hence, only one mask can be applied. And secondly, the mask is applied to whole the drawing, not to a particular part.
The second approach is to use a pair of PushOpacityMask and Pop operations (let’s just call it Push/Pop below). These are performed on a drawing context instance. Msdn offers hardly any information on this. This is the only information we could find: “The mask is applied to all subsequent drawing commands until it is removed by the Pop operation”
Internet resources do not give any additional description of the Push/Pop approach either. Let’s try to understand how Push/Pop works. In order to do this we will perform several experiments. We need four pictures. The first is shading.
The inverted shading:
The next picture is a nice background:
And a red circle to draw on the foreground:
What I’m going to do is to draw the background with the opacity mask and the red circle on the foreground with the inverted opacity mask.
1 //create an opacity brush 2 ImageBrush opacityMask = new ImageBrush(); 3 opacityMask.ImageSource = new BitmapImage(new Uri("mask.png")); 4 5 //create an inverted mask 6 ImageBrush invertedOpacityMask = new ImageBrush(); 7 invertedOpacityMask.ImageSource = new BitmapImage(new Uri("invertedMask.png")); 8 9 //load an image from file 10 ImageSource clouds = new BitmapImage(new Uri("clouds.png")); 11 //load the red circle from file 12 ImageSource redCircle = new BitmapImage(new Uri("redCircle.png")); 13 14 DrawingVisual drawingVisual = new DrawingVisual(); 15 DrawingContext dc = drawingVisual.RenderOpen(); 16 17 //push the opacity mask 18 dc.PushOpacityMask(opacityMask); 19 20 //draw the background 21 dc.DrawImage(clouds, new Rect(0, 0, clouds.Width, clowds.Height)); 22 dc.Pop(); 23 24 //push the inverted opacity mask 25 dc.PushOpacityMask(invertedOpacityMask); 26 //draw the red circle 27 dc.DrawImage(redCircle, new Rect((clouds.Width – redCircle.Width)/20, 0, redCircle.Width, redCircle.Height)); 28 dc.Pop(); 29 dc.Close();
Looks nice, isn’t it? That’s what I wanted. Notice, that we should call Pop() before we draw the red circle, otherwise the first mask would be applied to the red circle as well.
But it is not entirely clear for which area the mask is applied. Is it scaled to fit the whole image?
To take a better look at the matter I’ll take a transparent background image and draw a red square on it with the inverted shading as an opacity mask. Here is the result:
If the opacity mask is scaled to fit the picture, the red square should be fully opaque as it is placed in the fully dark part of the shade. We can see it from the schema below.
Apparently the opacity mask was applied to the red square only. The schema in this case should looks like this:
It looks like the opacity mask is being scaled to fit object which are drawn within Push/Pop scope. We will verify this by drawing a number of objects. You can see the result below:
Our assumption is correct – the opacity mask is scaled to fit the area where the objects have been drawn. The schema below represents it.
We have gained some insight how the WPF opacity mask can be used imperatively. We learned:
- If you want only one opacity mask to be applied to the whole Visual, you can assign an opacity mask to the Visual instance.
- If you want to apply more than one mask to the Visual, you should use a pair of PushOpacityMask and Pop operations on a drawing context instance.
- If you want to apply an opacity mask to a particular set of drawings on the Visual, you should draw the set within the scope of PushOpacityMask/Pop. Keep in mind though, that the mask will be scaled to fit the drawing area of the entire set of elements.
- You can also apply the opacity mask to each drawing individually and this will produce a different visual effect.