Compound paths
A compound path packs multiple GeometryPathType contours into one IDML shape — how a hole gets punched with opposite winding, and why the parser records contour boundaries.
A compound path is one IDML shape built from multiple contours — the way a hole gets punched into a fill.
In short: A single
PathGeometry can hold more than one
GeometryPathType contour. When it does, the contours together describe one
shape with multiple loops — a compound path. This is how IDML draws a shape
with a hole: a doughnut, the counter inside an "O", a frame with a window cut out
of it. Nothing marks a contour as "outer" or "inner"; whether the inner loop
carves a hole or adds a second blob comes down to its winding direction under
the nonzero winding rule. So the parser records where each contour begins, and
the renderer closes each one as its own loop.
Two contours, one shape
The idea is simple: an outer contour defines the filled region, and an inner
contour carves a hole out of it. Both are ordinary GeometryPathType elements
inside the same PathGeometry; nothing marks one as "outer" and one as "inner".
What decides whether the inner loop is a hole or just a second filled blob is its
winding direction.
InDesign fills paths with the nonzero winding rule. Two loops wound in the same direction reinforce each other (you get a solid shape); two loops wound in opposite directions cancel where they overlap (you get a hole). So punching a hole means winding the inner contour the opposite way from the outer one.
An outer square with an inner square wound the opposite way — the overlap cancels into a hole.
Spreads/Spread_uspread.xml<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<idPkg:Spread xmlns:idPkg="http://ns.adobe.com/AdobeInDesign/idml/1.0/packaging" DOMVersion="20.0">
<Spread Self="uspread" PageCount="1" BindingLocation="0" ShowMasterItems="true" AllowPageShuffle="true" ItemTransform="1 0 0 1 0 0">
<Page Self="upage" Name="1" AppliedMaster="umaster" ItemTransform="1 0 0 1 0 0" GeometricBounds="0 0 841.89 595.276" MasterPageTransform="1 0 0 1 0 0"/>
<Polygon Self="upoly" ItemTransform="1 0 0 1 150 250" FillColor="Color/Red" StrokeColor="Swatch/None" StrokeWeight="0">
<Properties>
<PathGeometry>
<GeometryPathType PathOpen="false">
<PathPointArray>
<PathPointType Anchor="0 0" LeftDirection="0 0" RightDirection="0 0"/>
<PathPointType Anchor="240 0" LeftDirection="240 0" RightDirection="240 0"/>
<PathPointType Anchor="240 240" LeftDirection="240 240" RightDirection="240 240"/>
<PathPointType Anchor="0 240" LeftDirection="0 240" RightDirection="0 240"/>
</PathPointArray>
</GeometryPathType>
<GeometryPathType PathOpen="false">
<PathPointArray>
<PathPointType Anchor="80 80" LeftDirection="80 80" RightDirection="80 80"/>
<PathPointType Anchor="80 160" LeftDirection="80 160" RightDirection="80 160"/>
<PathPointType Anchor="160 160" LeftDirection="160 160" RightDirection="160 160"/>
<PathPointType Anchor="160 80" LeftDirection="160 80" RightDirection="160 80"/>
</PathPointArray>
</GeometryPathType>
</PathGeometry>
</Properties>
</Polygon>
<TextFrame Self="uframe" ParentStory="ustory" PreviousTextFrame="n" NextTextFrame="n" ContentType="TextType" AppliedObjectStyle="ObjectStyle/$ID/[None]" Visible="true" Name="$ID/" ItemTransform="1 0 0 1 57.638 145.8237" FillColor="Swatch/None" StrokeColor="Swatch/None" StrokeWeight="0">
<Properties>
<PathGeometry>
<GeometryPathType PathOpen="false">
<PathPointArray>
<PathPointType Anchor="0 0" LeftDirection="0 0" RightDirection="0 0"/>
<PathPointType Anchor="0 400" LeftDirection="0 400" RightDirection="0 400"/>
<PathPointType Anchor="480 400" LeftDirection="480 400" RightDirection="480 400"/>
<PathPointType Anchor="480 0" LeftDirection="480 0" RightDirection="480 0"/>
</PathPointArray>
</GeometryPathType>
</PathGeometry>
</Properties>
</TextFrame>
</Spread>
</idPkg:Spread>
In the example, the outer contour runs 0,0 → 240,0 → 240,240 → 0,240 and the
inner contour is wound the reverse way. The result is a red square frame with a
clean white window in the middle. Reverse the inner contour's points so both wind
the same way and the hole fills back in.
Why contour boundaries have to be recorded
The reason compound paths get their own page is a quiet failure mode. The parser
flattens every contour's anchor points into one list, but it also records, for
each GeometryPathType, the index where that contour's points begin — a list
of subpath start offsets. The renderer uses those offsets to emit a separate
move-and-close per contour.
Without the boundaries, the renderer would have no way to tell where one loop ends and the next begins. It would join the last point of the outer square straight to the first point of the inner square with a stray segment, producing a single broken polyline instead of two clean loops — and the hole would never form. The contour boundaries are what keep the inner loop a distinct, separately-closed sub-path.
A shape with a single contour — the common case, every plain rectangle and simple
polygon — needs none of this: there is one loop, no boundaries to track, and the
renderer closes it directly. The boundary list only fills in when a second
GeometryPathType appears.
The same machinery carries open contours
Each contour also carries its own PathOpen
flag, recorded in parallel with the boundary offsets. So a compound path can mix
open and closed contours — one loop sealed, another left as a stroke — and each is
closed (or not) on its own terms. The boundaries and the open flags travel
together, contour for contour.
Frequently asked questions
What is a compound path in IDML?
A compound path is a single
PathGeometry containing more than
one GeometryPathType contour, where the contours together form one shape with
multiple loops. It is how IDML represents shapes with holes, such as a doughnut
or the counter inside the letter "O".
How does a compound path punch a hole? InDesign fills paths with the nonzero winding rule: two loops wound in the same direction reinforce into a solid shape, while two loops wound in opposite directions cancel where they overlap, leaving a hole. Punching a hole therefore means winding the inner contour the opposite way from the outer one — nothing flags one contour as "inner".
Why does the parser record contour boundaries?
The parser flattens all contour anchor points into one list, so it also records
the index where each GeometryPathType begins — a list of subpath start offsets.
The renderer uses those offsets to emit a separate move-and-close per contour;
without them it would join the last point of one loop to the first point of the
next with a stray segment, and the hole would never form.
PathGeometry
PathGeometry is the anchor-point outline of an IDML page item — nested GeometryPathType, PathPointArray, and PathPointType elements with Bézier handles, plus the PathOpen flag.
Layout model
IDML's layout model is the nest of containers that places content — a document of spreads, each holding pages, each filled by page items, with master spreads supplying repeating layout.