Non-preset <a:tileRect> behaves strange in case of gradient fill with <a:path>.

Regina Henschel 226 Reputation points
2025-04-14T20:30:39.85+00:00

If you set a custom <a:tileRect> for rectangle or radial gradient fill, then the rendering is strange.

I know, that the flip attribute of OOXML is not supported, but the description in Section 2.1.1297.a in [MS-OI29500] works neither.

If the tile rectangle does not cover the entire shape bounding box and is partly outside the shape bounding box, the rendering is totally broken, example 1 and 2 in screenshot.

If the tile rectangle is smaller than the shape bounding box and is located inside the shape bounding box, then the rendering does not follow the "flip" attribute as specified. Section 2.1.1297.a in [MS-OI29500] states, that flip="xy" is used. But for a radial gradient fill it looks like flip="none" (example 4 in screenshot). And a rectangular gradient extends the gradient, but does not fill the entire shape (example 3 in screenshot).

Might it be, that only the preset versions of <a:tileRect> are supported for now? If yes, [MS-OI29500] should mention it.

Screenshot_SmallTile_examples.png

I have sent the pptx-file to [email protected]. Subject:To Mike Bowen for question "Non-preset <a:tileRect> behaves strange in case of gradient fill with <a:path>".

Office Open Specifications
Office Open Specifications
Office: A suite of Microsoft productivity software that supports common business tasks, including word processing, email, presentations, and data management and analysis.Open Specifications: Technical documents for protocols, computer languages, standards support, and data portability. The goal with Open Specifications is to help developers open new opportunities to interoperate with Windows, SQL, Office, and SharePoint.
146 questions
{count} votes

5 answers

Sort by: Most helpful
  1. Mike Bowen 1,961 Reputation points Microsoft Employee
    2025-04-17T19:22:48.6166667+00:00

    Hi @Regina Henschel ,

    Below is a more detailed explanation of how gradients are calculated in PowerPoint. I am sharing the full article, because it may help answer additional questions related to rendering gradients that come up. It is a long post, so I have broken it up into several answers because of post size limits on Q&A. For this specific question, the relevant section is "Path Gradients". This doesn't discuss "flip" specifically but describes how our path-gradient algorithm breaks down for convex paths and weird tileRect settings, and how we defer to a radial gradient fill for many preset paths and shapes with custom geometry (like a scribble shape). I think this explains most or all weird rendering results that you shared. Please let me know if you have additional questions.


  2. Mike Bowen 1,961 Reputation points Microsoft Employee
    2025-04-17T19:23:25.1166667+00:00

    Intro

    OfficeArt can fill shapes with various types of gradient fills. Our desktop UI offers users four basic gradient choices: linear, radial, gradient, and path. Here are what each of these look like in their most basic form, all using the same 5-color gradient-stop list:

    linear radial rectangle path
    linear radial rectangle path

    In the OfficeArt OOXML file format, gradient fills are represented by the XML complex type element CT_GradientFillProperties. This type contains a gradient stop list (type CT_GradientTypeList) and then a choice of either CT_LinearShadeProperties (for a linear gradient) or CT_PathShadeProperties (for a radial, rectangle, or path gradients).

    These fill types look pretty straightforward at first, but they have several optional properties, and the way Office draws gradients is hard to reverse-engineer from our public OOXML spec. Several parts of this article were inspired by customer questions about how we implement our own native file format.

    Gradient Stop List

    All gradient fills have a gradient stop list, which define the list of colors that span the fill area. Each color is paired with a position value, which is a percentage describing where the color is at full intensity in the range of the gradient fill.

    Normal Interpolation Rules

    In general, the color at any position pos in the fill range is determined by all the gradient stop colors with this formula:

    If pos <= the min position of any color stop, posmin, then Color(pos) is the color of the stop at posmin. else if pos >= the max position of any color stop, posmax, then Color(pos) is the color of the stop at posmax. else if pos falls between two other color stop positions, pos is linearly interpolated between those two nearest colors.

    There are a few notable exceptions to this formula, described below.

    Empty Gradient Stop List

    If the gradient stop list is empty, the entire gradient fill range is filled with black.

    0 comments No comments

  3. Mike Bowen 1,961 Reputation points Microsoft Employee
    2025-04-17T19:23:54.3+00:00

    Two-color Gradients

    If the gradient stop list has two colors, which have positions of 0% and 100% respectively, then we use a fairly complicated interpolation strategy, where we blend each (r,g,b) color channel independently, using a gamma interpolation function, that is oriented so that the brighter of the two values dominates the area between the two colors.

    Here's C++ pseudocode to describe this interpolation, using C runtime 'pow' power function, which is to be called for each RGB color channel.

    // t: expresses a linear ratio beween color1 (value 0.0) and color2 (value 1.0) // rampUp: true if a color channel is growing lighter from color1 to color2, or false if it is growing darker. float ColorChannelVal(float t, bool rampUp) {   if(rampUp)       return 1.0f - pow( 1.0f - t, 1.875);    return pow(t, 1.875); }

    This function is used to convert the "linear" distance between colors to a "gamma" value for each RGB channel independently.  Then we interpolate the actual color channel value by using the gamma values as a ratio between val1 and val2, rather than the original linear value. For the alpha channel, we perform a linear interpolation between the colors, rather than using the gamma function.

    Note that we only apply this special algorithm when there are 2 colors, and one has a position of 0.0 and the other has a position of 1.0 across the gradient.  If the gradient has two colors but one has a position like, say, 0.2, then we default back to linear interpolation.

    This sample shows a two-color gradient where color channels are interpolated in different directions.

    two-color-sample

    This is what the same example looks like with a normal linear gradient applied (achieved by nudging the position of the right-most color stop from 100% to 99%). It is noticeably less bright:

    two-color-sample-normal

    Here is a visualization of the separate red, green, and blue components. Note how the red and blue components are oriented in opposite directions because the gamma function is applied to maximize the brightness along the width of the gradient range. The green channel is oriented the same direction as the red, but has much less variation between the opposite colors in the range.

    separated-colors

    Three-color Gradients

    If the gradient stop list has three colors, and the first and last color have the same color values, and are at positions 0% and 100%, respectively, then the colors are not linearly interpolated between the positions. Instead, we use the same per-channel interpolation logic as with the two-color gradient. The difference is that, in the 3-color case of S0, S1, S2, where S0.pos < S1.pos < S2.pos, and S0.color == S2.color, the 2-color interpolation is applied twice: once from S0 to S1, and a second time from S2 to S1.

    Below is a sample rendering with two 2-color gradients (shorter strips) drawn next to two 3-color gradients (longer strips).  It is easy to see the colors line up perfectly when the middle stop of the 3-color gradient lines up with the end of the 2-color gradient.

    3-vs-2-color-gradient

    Below is a 3-color gradient example with black and yellow gradient stops:

    3-color-gradient-black-and-yellow

    This is what the same gradient would look like with a simpler linear interpolation. Note there is a 'crease' in the middle of the range.

    3-color-gradient-black-and-yellow-simpler-interpolation

    We do not, but maybe we should, have any way to enable this per-channel gamma interpolation for 4+ gradient stops. That future feature would produce brighter and smoother gradients for more complex collections of colors.

    0 comments No comments

  4. Mike Bowen 1,961 Reputation points Microsoft Employee
    2025-04-17T19:25:11.72+00:00

    Definition of Path

    Each of the gradient types - radial, gradient, rectangle, path - fill shapes that are defined as paths. Here is a short primer on what paths are, that might make the next sections clearer.

    A path is an ordered list of one or more 2D segments.  Each segment is a collection of lines or curves, starting with a MoveTo instruction, followed by a series of one or more ArcTo/LineTo/BezierTo instructions, and finally an optional Close instruction that determines whether the segment is an (open) curve or a (closed) volumetric 2D space.

    The simplest path is a line:

    line

    The simplest closed paths have a single closed segment, with non-overlapping geometry (e.g. a rectangle or ellipse):

    rectangle-or-elipse

    Paths can have multiple closed sections (e.g. a "No" symbol has three closed segments):

    no-symbol

    A path can contain self-intersecting geometry:

    intersecting-geometry

    A path can have a combination of open and closed segments:

    frown-face

    Anchor and Focus Rectangles

    All gradient types fill an area relative to an "anchor rectangle". The path-related gradient types (but not the linear gradient type) make use of a secondary "focus" rectangle.

    The path attribute of CT_PathShadeProperties can be assigned three values: shape, circle, or rect.  Office UI and documentation often refers to these gradient types as "path", "radial", and "rectangular", respectively.  Behaviorally, each of these three gradient types are similar in that they all interpolate colors between two shapes, where the shapes are themselves positioned relative to two rectangles: an anchor rectangle and a focus rectangle.  The anchor rectangle is a property of the original shape itself, defined by the shape's xfrm (transform) attribute.  The focus rectangle is defined by the fillToRect attribute, which is inside the path attribute of the gradFill attribute.

    Below are some examples showing the anchor and focus rectangles for various schema combinations.

    focus rectangle = center point focus rectangle = off-center point focus rectangle = off-center rectangle focus rectangle = off-center line
    center-point off-center-point off-center-rectangle off-center-line
    <a:fillToRectl="50000"t="50000"r="50000"b="50000"/> <a:fillToRectl="80000"t="25000"r="20000"b="75000"/> <a:fillToRectl="25000"t="75000"r="25000"b="-25000"/> <a:fillToRectl="15000"t="20000"r="85000"b="20000"/>

    The anchor rectangle is the same in each case here, and the focus rectangle is defined as a "relative rectangle": each value is a high-precision percentage value that places an edge of the focus rectangle relative to the corresponding edge and width/height of the anchor rectangle.  For example, the bottom edge of the focus rectangle is placed relative to the bottom edge of the anchor rectangle. A bottom edge value of b=75000 means the edge is offset towards the center of the shape by 75% of the focus rectangle height. A bottom edge value of b=-25000 means the edge is offset away from the center of the shape by 25% of the focus rectangle height.


  5. Mike Bowen 1,961 Reputation points Microsoft Employee
    2025-04-17T19:25:56.1433333+00:00

    Linear Gradients

    When CT_GradientFillProperties.lin is defined, the fill is a linear gradient fill. The linear gradient has a couple attributes: an optional 'scaled' flag that we don't have UI for (so not discussed here), and an angle, that determines the orientation of the gradient fill.

    The following examples, with a Pride-month-inspired color stops, show gradients being applied at 0 degrees (top row of shapes), 45 degrees (middle), and 90 degrees (bottom). The gradient is interpolated between a range calculated from the rotation angle and the anchor rectangle bounds. Dotted lines by each example show the min/max range limits of the gradient, as it is interpolated across the shape. For a rectangular shape, where the shape path is the same as the anchor rectangle, all of the gradient colors appear in the shape (left column of diagram). When a linear gradient is applied to other shapes, the effect appears as if the shape was a stencil used to crop out the rectangular gradient.

    linear-gradients

    Rectangle Gradients

    The default value for CT_GradientFillProperties.path is "rect", and this type is also the easiest to explain.  The gradient colors are interpolated smoothly between the focus and anchor rectangles.  If the focus rect has area (i.e. it is not a point and not a line), it will be filled with the first color of the gradient.

    Using the same four examples of various gradient focus rectangles, here is how gradients should be drawn with CT_PathShadeProperties.path = rect:

    rectangle-gradients-1 rectangle-gradients-2 rectangle-gradients-3 rectangle-gradients-4

    Path Gradients

    Path Gradients with CT_GradientFillProperties.path = "shape" are similar to the "rect" case; the gradient colors are interpolated smoothly between two shapes: one sized to fit into the focus rectangle, and a second sized to fit in the anchor rectangle.  If the focus rect has area (i.e. it is not a point and not a line), the shape in the focus rectangle will also be filled with the first color of the gradient.

    Using the same four examples of various gradient focus rectangles, here is how gradients should be drawn with CT_PathShadeProperties.path = path:

    path-gradients-1 path-gradients-2 path-gradients-3 path-gradients-4

    Note that, depending on exactly how an app implements the interpolation between two shapes, there will likely be ambiguity about what color to draw for many pixels.  This is especially true if the path is convex (has one or more interior angles > 180 degrees), as in this example:

    interior-angles

    There is more than one way to try and interpolate a gradient fill between two shapes, but the method Office uses in most cases is to divide the area connecting the focus and anchor shapes into quadrilaterals so that opposite edges of each quad are at the same relative position on each shape.  The quads are then divided into two triangles, which are filled with a linear gradient (a simpler shape filling type that is supported by most modern graphics APIs).  When this strategy is applied to convex shapes, artifacts arise when different quads overlap.  The final result can be asymmetrical, and depends on the order the triangles were drawn.

    Because of asymmetrical rendering artifacts like this, Office chooses to render path gradients for many preset shapes as if they were CT_GradientFillProperties.path = "circle".  Office will also defer to radial gradient if the path is custom (not a preset type).

    Radial Gradients

    Radial path gradients are drawn when the path attribute of CT_PathShadeProperties is set to "circle".  The gradient colors are interpolated smoothly between a circle that circumscribes (outer) anchor rectangle, and an ellipse.  The ellipse is harder to describe; it fills the rectangle formed when the fillToRect relative rectangle is applied to the circle's bounds.

    Using the same examples of gradient focus rectangles, here are the circumscribed ellipses that radial gradients will interpolate between:

    radial-gradients-1 radial-gradients-2 radial-gradients-3 radial-gradients-4

    In these diagrams, a circle (identical in each) circumscribes the anchor rectangle, and the bounding box of this circle is shown by a long-dashed square.  In the third and fourth diagrams, dot-dash rectangles indicate the bounding box of the fillToRect rectangle, which is relative to the circle's bounding rectangle rather than the original anchor rectangle.

    Note that the inner ellipse doesn't fall in exactly the same location as with the rectangular path gradient, because the inner shape is relative to the circumscribed circle.

    Here are the resulting images when the gradient is interpolated between these circles/ellipses:

    interpolated-circle-elipse-1 interpolated-circle-elipse-2 interpolated-circle-elipse-3 interpolated-circle-elipse-4

    Radial Focus Point

    2D rendering tools such as GDI+ and D2D have built-in mechanisms for drawing gradients, and they often present the concept of "focus point" - the 2D point where the gradient radiates away from. Our algorithm for calculating the radial gradient focus point is relative to the anchor rectangle and the focus rectangle.

    Our PathGradientInfo takes in an anchor rectangle and a focus rectangle for determining the size and position of our inner path.  On the other hand, GDI+ takes in a center point and a scaling factor pair, so that the focus path can be determined by scaling the filled path about this center point.  We have to convert our rectangles into this representation, and these equations show how:

    Let P be the required center point and S be the required scaling factors in two dimensions.

    Let X and X' be the top-left corner of our outer and inner rectangles, respectively, and D and D' be the size of our outer and inner rectangles in two dimensions respectively.

    Then we must have:

                      S = D' / D

      ( X - P ) * S + P = X'

    Solving these equations for P,

        P = ( X' - S * X ) / ( 1 - S )

          = ( D * X' - D' * X ) / ( D - D' )

          = X' + D' * ( X' - X ) / ( D - D' )

    Therefore, we use this to perform our conversions:

        S = D' / D

        P = X' + D' * ( X' - X ) / ( D - D' )

    Here is a more literal translation of our currently shipping logic, in C++-like pseudo-code.  Here, "rcInner" is the focus rect, and "rcOuter" is the circumscribed rectangle around the shape bounds.

    Point2D ITech::CalculateGradientOrigin(const Rect& rcInner, const Rect& rcOuter)   {   Point2D gradientOrigin = Point2D(rcInner.left, rcInner.top);   Vector2D innerRectSize = rcInner.GetSize();   Vector2D outerRectSize = rcOuter.GetSize();   if( innerRectSize.dx > 0.0 )      {      double widthDiff = outerRectSize.dx - innerRectSize.dx;     if(NotEqualToZero(widthDiff)) // This helper function performs comparison using tolerance of 2 * DBL_EPSILON        gradientOrigin.x += innerRectSize.dx * (rcInner.left - rcOuter.left) / widthDiff;      }   if( innerRectSize.dy > 0.0 )      {      double heightDiff = outerRectSize.dy - innerRectSize.dy;     if(NotEqualToZero(heightDiff))        gradientOrigin.y += innerRectSize.dy * (rcInner.top - rcOuter.top) / heightDiff;      }   return gradientOrigin;   }

    0 comments No comments

Your answer

Answers can be marked as Accepted Answers by the question author, which helps users to know the answer solved the author's problem.