Last week we explained our work with theme color preservation for fonts and paragraphs, and now we move to a very close topic: the preservation of theme colors and styles on shapes.
DrawingML includes new conventions to denominate colors, wider than the initial OOXML spec as used for paragraphs and fonts. Such color definitions can be used in the context of shape properties or style definitions to define fillings, line colors, gradients…
In first place, English color names (red, black, etc.) can be used besides RGB values and theme color names; and for any color, a set of transformations can be defined, similarly to the tint and shade properties in fonts and paragraph colors, but with more options. An important transformation is alpha to define the transparency of that color.
Find below one example of each color tag (for color names, RGB values or theme colors), with or without transformations:
<!-- color by name, with an alpha transform --> <a:prstClr val="black"> <a:alpha val="50000"/> </a:prstClr> <!-- color by RGB value, no transforms --> <a:srgbClr val="FF0000"/> <!-- theme color, with two transforms --> <a:schemeClr val="accent1"> <a:satMod val="175000"/> <a:alpha val="40000"/> </a:schemeClr>
Shapes in DrawingML receive their attributes from two different sources: the style definitions and the own shape properties. The former act as a fallback of the latter; attributes from style definitions will only be applied if they are not overwritten by shape properties. For example, in case a shape contains both a filling definition among its properties and a filling style, the filling definition prevails.
The theme file defines a list of filling and line styles so they can be used by shapes. This is an example of an area filling style which adds a couple of color transformations:
<a:fillStyleLst> <a:solidFill> <a:schemeClr val="phClr"> <a:tint val="15000"/> <a:satMod val="350000"/> </a:schemeClr> </a:solidFill> <!-- more fill style definitions --> </a:fillStyleLst>
Notice it’s not using any particular color, phClr is an entry parameter of sorts for a color that will be indicated in the shape style definition. The style definition can add some transformations or even define a gradient fill based on that color.
A line style definition can be a bit more complex, because it has more format options: width, dash pattern, single or double… These options are available for shape properties too.
<a:lnStyleLst> <a:ln w="9525" cap="flat" cmpd="sng" algn="ctr"> <a:solidFill> <a:schemeClr val="phClr"/> </a:solidFill> <a:prstDash val="solid"/> </a:ln> <!-- more line style definitions --> </a:lnStyleLst>
This is how a shape from the document would be assigned to the first filling style and the second line style, specifying “accent1” and “accent2” colors as parameters:
<wps:style> <a:lnRef idx="2"> <!-- use the second element from fillStyleLst --> <a:schemeClr val="accent1"/> </a:lnRef> <a:fillRef idx="1"> <!-- use the first element from lnStyleLst --> <a:schemeClr val="accent2"/> </a:fillRef> ... </wps:style>
Finally, this is how a shape can overwrite the style-defined attributes for its filling and line. The following XML chunk defines a solid filling for the area using “accent3” color and a solid color for the shape line using “accent4”:
<wps:spPr> <!-- shape properties tag --> ... <a:solidFill> <!-- area filling --> <a:schemeClr val="accent3"/> </a:solidFill> <a:ln> <!-- line properties --> <a:solidFill> <a:schemeClr val="accent4"/> </a:solidFill> </a:ln> ... </wps:spPr>
The importer code
The duty of the importer code is, in general, applying the style definitions, merging them with the shape properties and transforming the properties in a way that matches LibreOffice internal data model.
Applying the style definitions means getting the corresponding styles from the theme file and applying them to the specific colors indicated in the shape wps:style block. This results in a set of properties that has to be merged with the shape-specific ones, and this is done so the latter have priority, as explained before.
All properties must be transformed to match LibreOffice data model, and specially colors because of their complexity. Theme colors are translated to their RGB value checking the theme definition to match the color name with its value. As for color transformations, the importer applies all the transformations to the original color to get a final RGB value. The only exception is the alpha transformation, which is converted in a transparency value to be stored in the fill properties object (alpha and transparency are complementary; after homogenizing their units, alpha = 1 – transparency).
LibreOffice doesn’t have equivalent concepts to theme colors and color transformations other than alpha, and doesn’t have shape styles either. To prevent information loss, we will use a hidden property in the Shape object called the interop grab bag to fill it with any properties we need to rebuild this information on export:
- To preserve a theme color, we need to store its name and the complete list of transformations. We will also need the RGB value of the color with all the transformations applied, so we can compare it with the final color to know if the user has changed it. We do it both for line and filling colors.
- For preset and RGB colors, the loss of the transformations is not important so we don’t consider this special case.
- To preserve the style definitions, we store the idx attribute of each and their color parameter, with all transformations if necessary. We also store the RGB value of the color including transformations because we will need it too.
The exporter code
The normal behavior of the exporter is translating every LibreOffice shape property to DrawingML to write it to the document. In the case of colors, that means using a a:srgbClr tag to save it in RGB format and optionally adding an alpha transformation calculated from the shape transparency value.
The preservation of shape styles, area and line colors increases a bit the complexity. The shape grab bag must be checked for interoperability information saved in the import phase which should be saved back to the document under certain circumstances.
If there are any style definitions in the shape grab bag, they are always written back to the document; we need no additional verification because style definitions act as a fallback as we already explained. On the other hand, the case of shape properties has some complexity because we must perform several checks before deciding which area and line information we want to save:
- Does the final color match the original one? If it doesn’t, it means the user has changed it during edition, so the new one must be saved and the information in the grab bag discarded.
- If the original and final colors match, is there any theme color information in the grab bag? If there is, we must write it to the shape properties.
- If the original and final colors match and there is no theme color information in the grab bag, we must compare the color with the style color; if they match, it means the shape was using the style color so we must not write a shape property that would overwrite it.
- Otherwise, the shape was using a custom color different from the style color and must be written to the shape properties.
Bonus track: multiple color gradients
While I was working on this topic, I noticed that DrawingML allows to specify gradient fillings with any number of steps, unlike in previous MS Office documents (and LibreOffice ones) where you could only use two. This is an example of gradient with three steps:
<wps:spPr> ... <a:gradFill> <a:gsLst> <a:gs pos="0"> <a:srgbClr val="ffff00" /> </a:gs> <a:gs pos="50000"> <a:srgbClr val="ffff33"> <a:alpha val="20000" /> </a:srgbClr> </a:gs> <a:gs pos="100000"> <a:srgbClr val="ff0000" /> </a:gs> </a:gsLst> <a:lin ang="5400000" /> </a:gradFill> ... </wps:spPr>
Every a:gs tag indicates a point in the gradient extension with the pos attribute, measured as a percentage, and the color of that point. The system should calculate the gradient between the colors of two consecutive points.
LibreOffice does an approximation of those gradients with more than two steps using only two colors when it imports the document. To prevent information loss, we use again the grab bag to store the complete gradient definition. In an analogous way, we store the original, approximated gradient information to be able to compare it with the gradient information in the moment of the save operation.
We can identify three situations in the exporter:
- The original and final gradients don’t match: it means the user has changed it during edition, so the new one must be saved and the information in the grab bag discarded.
- The original and final gradients match and there is a full gradient definition stored in the grab bag: we must write that gradient definition to the shape properties.
- The original and final gradients match and there isn’t any gradient definition stored in the grab bag: the gradient definition comes from the shape style and in that case we must not write the shape property that would, again, overwrite it.
One more thing or two
As you can see, this feature relies on the hidden interop grab bag property which contains more and more information as we increase its use to preserve unsupported properties. We hope to reduce its weight as those properties become natively supported. I would also like to comment that we have complemented our work with a set of unit tests that will help to detect regressions appearing in the future.
We haven’t yet finished with shapes; next post will be related to the preservation of different kinds of shape effects. It will be ready soon!