cool#16078: OOXML round-trip of the clip polygon
A placeholder clipped to an outline carries it as custom geometry, which an image shape has nowhere to keep, so the outline was dropped on load and the save wrote a plain rectangle over it. Read it into GraphicClipPolyPolygon, and write it back from there. The two directions are not equally capable. Import reads paths of literal coordinates: one driven by guides or formulas, and an arc, both need the custom-shape machinery. A quadratic curve is read, converting exactly to the cubic the model keeps. A path that states no w and h is not read either - such coordinates are absolute EMU rather than a space to scale from, and no file authored that way turned up to check the reading against. Export writes whatever polygon the model holds, so that direction loses nothing - a geometry the import gave up on was never stored. A curve that closes a contour states no end point of its own, and the point the contour started from answers for it. A path that states fill="none" states that it paints nothing, and PowerPoint leaves it out of the clip: a picture clipped by two paths shows through the plain one and not through the nofill one, while the same file with that one attribute removed - or with stroke="0" in its place - shows through both. So such a path is read past, while the shading variants and stroke="0" are not: those say how an area is painted, not whether there is one. Several paths become one polygon by area, not by concatenation: the contours of one path are filled by the even-odd rule, so two of them overlapping leave a hole, while paths are filled one after another and the shape covers what they cover together. Concatenating them instead cut a hole wherever two paths overlap, which the file does not describe and PowerPoint does not draw. A shape's own custom geometry now replaces the one it inherits, the way a preset geometry already did. Saving states the outline on the slide as well as on its layout, so reading that back gave the slide placeholder its own geometry beside the inherited one, and two geometries at once are not one this reads - the placeholder came back unclipped while PowerPoint showed the same file correctly. The outline reaches the drawing the commit before this one added, so a placeholder read from a file shows what sits behind the parts it does not cover. It survives a round-trip through ODF as well, where the property already travels as coext:graphic-clip-poly, with nothing to carry until now. Signed-off-by: Mike Kaganski <mike.kaganski@collabora.com> Change-Id: I87bd21e68bcea648c8c9151b6af5439578f1c2cf Reviewed-on: https://gerrit.collaboraoffice.com/c/online/+/9303 Tested-by: Jenkins CPCI <releng@collaboraoffice.com> Reviewed-by: Caolán McNamara <caolan.mcnamara@collabora.com> (cherry picked from commit 04bbb74a24bad4ae54f257b6b028f3fd48955272) Reviewed-on: https://gerrit.libreoffice.org/c/core/+/210123 Tested-by: Jenkins Reviewed-by: Xisco Fauli <xiscofauli@libreoffice.org>
This commit is contained in:
committed by
Xisco Fauli
parent
5e3fe53bdb
commit
1dc3b23162
@@ -525,6 +525,14 @@ public:
|
||||
const css::uno::Reference<css::drawing::XShape>& rXShape,
|
||||
const SdrObjCustomShape& rSdrObjCustomShape);
|
||||
void WriteEmptyCustomGeometry();
|
||||
/** Writes a graphic object's clip polygon as the shape's own geometry.
|
||||
|
||||
Returns false when the shape has no clip polygon, so that the caller can write the geometry
|
||||
it would have written anyway.
|
||||
*/
|
||||
OOX_DLLPUBLIC bool WriteGraphicClipCustomGeometry(
|
||||
const css::uno::Reference<css::beans::XPropertySet>& rXPropSet,
|
||||
const css::awt::Size& rSize);
|
||||
void WritePolyPolygon(const css::uno::Reference<css::drawing::XShape>& rXShape,
|
||||
const bool bClosed);
|
||||
OOX_DLLPUBLIC void WriteFill(const css::uno::Reference<css::beans::XPropertySet>& xPropSet,
|
||||
|
||||
@@ -19,6 +19,9 @@
|
||||
|
||||
#include <config_wasm_strip.h>
|
||||
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
|
||||
#include <oox/drawingml/shape.hxx>
|
||||
#include <drawingml/customshapeproperties.hxx>
|
||||
#include <oox/drawingml/theme.hxx>
|
||||
@@ -84,6 +87,10 @@
|
||||
#include <com/sun/star/drawing/GraphicExportFilter.hpp>
|
||||
#include <com/sun/star/drawing/XShapes.hpp>
|
||||
#include <com/sun/star/drawing/EnhancedCustomShapeAdjustmentValue.hpp>
|
||||
#include <com/sun/star/drawing/EnhancedCustomShapeParameterType.hpp>
|
||||
#include <com/sun/star/drawing/EnhancedCustomShapeSegmentCommand.hpp>
|
||||
#include <com/sun/star/drawing/PolygonFlags.hpp>
|
||||
#include <com/sun/star/drawing/PolyPolygonBezierCoords.hpp>
|
||||
#include <com/sun/star/drawing/XEnhancedCustomShapeDefaulter.hpp>
|
||||
#include <com/sun/star/drawing/EnhancedCustomShapeTextPathMode.hpp>
|
||||
#include <com/sun/star/drawing/ConnectorType.hpp>
|
||||
@@ -102,6 +109,9 @@
|
||||
|
||||
#include <basegfx/point/b2dpoint.hxx>
|
||||
#include <basegfx/polygon/b2dpolygon.hxx>
|
||||
#include <basegfx/polygon/b2dpolypolygon.hxx>
|
||||
#include <basegfx/polygon/b2dpolypolygoncutter.hxx>
|
||||
#include <basegfx/polygon/b2dpolypolygontools.hxx>
|
||||
#include <basegfx/matrix/b2dhommatrix.hxx>
|
||||
#include <com/sun/star/document/XActionLockable.hpp>
|
||||
#include <com/sun/star/chart2/data/XDataReceiver.hpp>
|
||||
@@ -920,6 +930,185 @@ void lcl_RotateAtCenter(basegfx::B2DHomMatrix& aTransformation, sal_Int32 nMSORo
|
||||
return;
|
||||
}
|
||||
|
||||
/** Reads a <a:custGeom> as a clip polygon, in the shape's own coordinates and 1/100 mm.
|
||||
|
||||
A placeholder that is clipped to an outline carries that outline as custom geometry, which an
|
||||
image shape has nowhere to keep - GraphicClipPolyPolygon is where it goes. Only literal
|
||||
coordinates are read: a path driven by guides or formulas needs the whole custom-shape
|
||||
machinery, and returning nothing then leaves the shape rectangular, as it was before.
|
||||
*/
|
||||
std::optional<drawing::PolyPolygonBezierCoords>
|
||||
lcl_getClipPolygon(CustomShapeProperties& rProperties, const awt::Rectangle& rShapeRect)
|
||||
{
|
||||
const std::vector<Path2D>& rPaths = rProperties.getPath2DList();
|
||||
if (rPaths.empty())
|
||||
return {};
|
||||
|
||||
auto readValue = [](const drawing::EnhancedCustomShapeParameter& rParameter, double& rfValue)
|
||||
{
|
||||
if (rParameter.Type != drawing::EnhancedCustomShapeParameterType::NORMAL)
|
||||
return false;
|
||||
if (double fValue; rParameter.Value >>= fValue)
|
||||
rfValue = fValue;
|
||||
else if (sal_Int32 nValue; rParameter.Value >>= nValue)
|
||||
rfValue = nValue;
|
||||
else
|
||||
return false;
|
||||
return true;
|
||||
};
|
||||
|
||||
// The segments of every path arrive in one list, each path's ended by an ENDSUBPATH, while the
|
||||
// points belong to the path they were read with - so the two are walked in step.
|
||||
std::vector<basegfx::B2DPolyPolygon> aPaths;
|
||||
basegfx::B2DPolyPolygon aPath;
|
||||
basegfx::B2DPolygon aContour;
|
||||
size_t nPath = 0;
|
||||
size_t nPoint = 0;
|
||||
|
||||
auto takePoint = [&](basegfx::B2DPoint& rPoint)
|
||||
{
|
||||
if (nPath >= rPaths.size() || nPoint >= rPaths[nPath].parameter.size())
|
||||
return false;
|
||||
const Path2D& rCurrent = rPaths[nPath];
|
||||
double fX = 0.0;
|
||||
double fY = 0.0;
|
||||
if (rCurrent.w <= 0 || rCurrent.h <= 0
|
||||
|| !readValue(rCurrent.parameter[nPoint].First, fX)
|
||||
|| !readValue(rCurrent.parameter[nPoint].Second, fY))
|
||||
return false;
|
||||
++nPoint;
|
||||
rPoint = basegfx::B2DPoint(fX * rShapeRect.Width / rCurrent.w,
|
||||
fY * rShapeRect.Height / rCurrent.h);
|
||||
return true;
|
||||
};
|
||||
|
||||
auto endContour = [&]()
|
||||
{
|
||||
if (aContour.count())
|
||||
{
|
||||
aContour.setClosed(true);
|
||||
aPath.append(aContour);
|
||||
}
|
||||
aContour.clear();
|
||||
};
|
||||
|
||||
bool bPathIsFilled = true;
|
||||
|
||||
auto endPath = [&]()
|
||||
{
|
||||
endContour();
|
||||
// A path that paints nothing covers nothing: PowerPoint leaves it out of the clip, which
|
||||
// three renders differing only in this attribute confirm. How the outline is stroked says
|
||||
// nothing about the area, so nostroke is not this.
|
||||
if (aPath.count() && bPathIsFilled)
|
||||
aPaths.push_back(aPath);
|
||||
aPath.clear();
|
||||
bPathIsFilled = true;
|
||||
++nPath;
|
||||
nPoint = 0;
|
||||
};
|
||||
|
||||
for (const drawing::EnhancedCustomShapeSegment& rSegment : rProperties.getSegments())
|
||||
{
|
||||
basegfx::B2DPoint aPoint;
|
||||
switch (rSegment.Command)
|
||||
{
|
||||
case drawing::EnhancedCustomShapeSegmentCommand::MOVETO:
|
||||
endContour();
|
||||
if (!takePoint(aPoint))
|
||||
return {};
|
||||
aContour.append(aPoint);
|
||||
break;
|
||||
case drawing::EnhancedCustomShapeSegmentCommand::LINETO:
|
||||
for (sal_Int32 i = 0; i < rSegment.Count; ++i)
|
||||
{
|
||||
if (!aContour.count() || !takePoint(aPoint))
|
||||
return {};
|
||||
aContour.append(aPoint);
|
||||
}
|
||||
break;
|
||||
case drawing::EnhancedCustomShapeSegmentCommand::CURVETO:
|
||||
for (sal_Int32 i = 0; i < rSegment.Count; ++i)
|
||||
{
|
||||
basegfx::B2DPoint aFirst;
|
||||
basegfx::B2DPoint aSecond;
|
||||
if (!aContour.count() || !takePoint(aFirst) || !takePoint(aSecond)
|
||||
|| !takePoint(aPoint))
|
||||
return {};
|
||||
aContour.appendBezierSegment(aFirst, aSecond, aPoint);
|
||||
}
|
||||
break;
|
||||
case drawing::EnhancedCustomShapeSegmentCommand::QUADRATICCURVETO:
|
||||
// A quadratic curve is exactly the cubic one whose two controls sit two thirds of
|
||||
// the way from each end towards the single control the file gives.
|
||||
for (sal_Int32 i = 0; i < rSegment.Count; ++i)
|
||||
{
|
||||
basegfx::B2DPoint aControl;
|
||||
if (!aContour.count() || !takePoint(aControl) || !takePoint(aPoint))
|
||||
return {};
|
||||
const basegfx::B2DPoint aFrom(aContour.getB2DPoint(aContour.count() - 1));
|
||||
aContour.appendBezierSegment(aFrom + (aControl - aFrom) * (2.0 / 3.0),
|
||||
aPoint + (aControl - aPoint) * (2.0 / 3.0),
|
||||
aPoint);
|
||||
}
|
||||
break;
|
||||
case drawing::EnhancedCustomShapeSegmentCommand::ENDSUBPATH:
|
||||
endPath();
|
||||
break;
|
||||
case drawing::EnhancedCustomShapeSegmentCommand::NOFILL:
|
||||
bPathIsFilled = false;
|
||||
break;
|
||||
case drawing::EnhancedCustomShapeSegmentCommand::CLOSESUBPATH:
|
||||
// Shading and stroking say nothing about the area a path covers.
|
||||
case drawing::EnhancedCustomShapeSegmentCommand::NOSTROKE:
|
||||
case drawing::EnhancedCustomShapeSegmentCommand::DARKEN:
|
||||
case drawing::EnhancedCustomShapeSegmentCommand::DARKENLESS:
|
||||
case drawing::EnhancedCustomShapeSegmentCommand::LIGHTEN:
|
||||
case drawing::EnhancedCustomShapeSegmentCommand::LIGHTENLESS:
|
||||
break;
|
||||
default:
|
||||
return {};
|
||||
}
|
||||
}
|
||||
endPath();
|
||||
|
||||
if (aPaths.empty())
|
||||
return {};
|
||||
|
||||
// The contours of one path are filled by the even-odd rule - two that overlap leave a hole,
|
||||
// which is what PowerPoint draws - and that is the rule a clip is applied with, so one path is
|
||||
// kept exactly as it was read.
|
||||
basegfx::B2DPolyPolygon aClip(aPaths.front());
|
||||
if (aPaths.size() > 1)
|
||||
{
|
||||
// Paths, in contrast, are filled one by one, so the shape covers what they cover together.
|
||||
// Combining them needs each path's own area first, which is that even-odd rule spelled out
|
||||
// as a boolean expression over its contours.
|
||||
auto contour = [](const basegfx::B2DPolyPolygon& rPath, sal_uInt32 nIndex)
|
||||
{
|
||||
// The operations below expect their operands oriented, which is what preparing does.
|
||||
return basegfx::utils::prepareForPolygonOperation(
|
||||
basegfx::B2DPolyPolygon(rPath.getB2DPolygon(nIndex)));
|
||||
};
|
||||
|
||||
auto areaOf = [&contour](const basegfx::B2DPolyPolygon& rPath)
|
||||
{
|
||||
basegfx::B2DPolyPolygon aArea(contour(rPath, 0));
|
||||
for (sal_uInt32 i = 1; i < rPath.count(); ++i)
|
||||
aArea = basegfx::utils::solvePolygonOperationXor(aArea, contour(rPath, i));
|
||||
return aArea;
|
||||
};
|
||||
|
||||
aClip = areaOf(aPaths.front());
|
||||
for (size_t i = 1; i < aPaths.size(); ++i)
|
||||
aClip = basegfx::utils::solvePolygonOperationOr(aClip, areaOf(aPaths[i]));
|
||||
}
|
||||
|
||||
drawing::PolyPolygonBezierCoords aCoords;
|
||||
basegfx::utils::B2DPolyPolygonToUnoPolyPolygonBezierCoords(aClip, aCoords);
|
||||
return aCoords;
|
||||
}
|
||||
|
||||
Degree100 lcl_MSORotateAngleToAPIAngle(const sal_Int32 nMSORotationAngle)
|
||||
{
|
||||
// Converts a shape rotation angle from MSO to angle for API property RotateAngle
|
||||
@@ -2228,8 +2417,17 @@ Reference< XShape > const & Shape::createAndInsert(
|
||||
// would make the object behave like a standard outline object. An authored prompt
|
||||
// reaches the object through the CustomPromptText property instead.
|
||||
if (rServiceName == "com.sun.star.presentation.GraphicObjectShape")
|
||||
{
|
||||
mpTextBody.reset();
|
||||
|
||||
// The custom geometry of a picture placeholder is the outline it is clipped to. An
|
||||
// image shape has no geometry of its own to put it in, so it becomes a clip polygon;
|
||||
// making the shape a custom shape instead, as a cropped image does, would cost the
|
||||
// placeholder its identity.
|
||||
if (auto oClip = lcl_getClipPolygon(*mpCustomShapePropertiesPtr, aShapeRectHmm))
|
||||
aPropertySet.setAnyProperty(PROP_GraphicClipPolyPolygon, Any(*oClip));
|
||||
}
|
||||
|
||||
// in some cases, we don't have any text body.
|
||||
if( mpTextBody && ( !pPlaceholder || !mpTextBody->isEmpty() ) )
|
||||
{
|
||||
|
||||
@@ -55,6 +55,8 @@ ContextHandlerRef ShapePropertiesContext::onCreateContext( sal_Int32 aElementTok
|
||||
|
||||
// GeometryGroup
|
||||
case A_TOKEN( custGeom ): // custom geometry "CT_CustomGeometry2D"
|
||||
// We got a custom geometry, forget the geometry inherited from the placeholder shape.
|
||||
mrShape.getCustomShapeProperties() = std::make_shared<CustomShapeProperties>();
|
||||
return new CustomShapeGeometryContext( *this, *mrShape.getCustomShapeProperties() );
|
||||
|
||||
case A_TOKEN( prstGeom ): // preset geometry "CT_PresetGeometry2D"
|
||||
|
||||
@@ -68,6 +68,7 @@
|
||||
#include <com/sun/star/drawing/ColorMode.hpp>
|
||||
#include <com/sun/star/drawing/EnhancedCustomShapeAdjustmentValue.hpp>
|
||||
#include <com/sun/star/drawing/EnhancedCustomShapeParameterPair.hpp>
|
||||
#include <com/sun/star/drawing/PolyPolygonBezierCoords.hpp>
|
||||
#include <com/sun/star/drawing/EnhancedCustomShapeParameterType.hpp>
|
||||
#include <com/sun/star/drawing/EnhancedCustomShapeSegment.hpp>
|
||||
#include <com/sun/star/drawing/EnhancedCustomShapeSegmentCommand.hpp>
|
||||
@@ -5829,6 +5830,86 @@ void DrawingML::WriteEmptyCustomGeometry()
|
||||
mpFS->endElementNS(XML_a, XML_custGeom);
|
||||
}
|
||||
|
||||
bool DrawingML::WriteGraphicClipCustomGeometry(const Reference<XPropertySet>& rXPropSet,
|
||||
const awt::Size& rSize)
|
||||
{
|
||||
if (!rXPropSet.is()
|
||||
|| !rXPropSet->getPropertySetInfo()->hasPropertyByName(
|
||||
u"GraphicClipPolyPolygon"_ustr))
|
||||
return false;
|
||||
|
||||
drawing::PolyPolygonBezierCoords aClip;
|
||||
if (!(rXPropSet->getPropertyValue(u"GraphicClipPolyPolygon"_ustr) >>= aClip)
|
||||
|| !aClip.Coordinates.hasElements())
|
||||
return false;
|
||||
|
||||
mpFS->startElementNS(XML_a, XML_custGeom);
|
||||
mpFS->singleElementNS(XML_a, XML_avLst);
|
||||
mpFS->singleElementNS(XML_a, XML_gdLst);
|
||||
mpFS->singleElementNS(XML_a, XML_ahLst);
|
||||
mpFS->singleElementNS(XML_a, XML_rect, XML_l, "0", XML_t, "0", XML_r, "r", XML_b, "b");
|
||||
mpFS->startElementNS(XML_a, XML_pathLst);
|
||||
|
||||
// The polygon is in the shape's own coordinates, so the path spans the shape and a point needs
|
||||
// no conversion. All the contours share one path, which is what subtracts the areas they
|
||||
// overlap - that is how a frame with a hole in it is described.
|
||||
mpFS->startElementNS(XML_a, XML_path, XML_w, OString::number(rSize.Width), XML_h,
|
||||
OString::number(rSize.Height));
|
||||
|
||||
auto writePoint = [this](const awt::Point& rPoint) {
|
||||
mpFS->singleElementNS(XML_a, XML_pt, XML_x, OString::number(rPoint.X), XML_y,
|
||||
OString::number(rPoint.Y));
|
||||
};
|
||||
|
||||
for (sal_Int32 nContour = 0; nContour < aClip.Coordinates.getLength(); ++nContour)
|
||||
{
|
||||
const Sequence<awt::Point>& rPoints = aClip.Coordinates[nContour];
|
||||
// A contour may carry no flags at all; every point is then an ordinary one.
|
||||
const Sequence<drawing::PolygonFlags> aNoFlags;
|
||||
const Sequence<drawing::PolygonFlags>& rFlags
|
||||
= nContour < aClip.Flags.getLength() ? aClip.Flags[nContour] : aNoFlags;
|
||||
if (!rPoints.hasElements())
|
||||
continue;
|
||||
|
||||
mpFS->startElementNS(XML_a, XML_moveTo);
|
||||
writePoint(rPoints[0]);
|
||||
mpFS->endElementNS(XML_a, XML_moveTo);
|
||||
|
||||
for (sal_Int32 nPoint = 1; nPoint < rPoints.getLength(); ++nPoint)
|
||||
{
|
||||
const bool bControl = nPoint < rFlags.getLength()
|
||||
&& rFlags[nPoint] == drawing::PolygonFlags_CONTROL;
|
||||
if (!bControl)
|
||||
{
|
||||
mpFS->startElementNS(XML_a, XML_lnTo);
|
||||
writePoint(rPoints[nPoint]);
|
||||
mpFS->endElementNS(XML_a, XML_lnTo);
|
||||
continue;
|
||||
}
|
||||
|
||||
// A curve is two control points and the point they lead to. The one that closes a
|
||||
// contour leads back to the point the contour started from and states no end point of
|
||||
// its own, so the start point answers for it.
|
||||
if (nPoint + 1 >= rPoints.getLength())
|
||||
break;
|
||||
const bool bClosing = nPoint + 2 >= rPoints.getLength();
|
||||
mpFS->startElementNS(XML_a, XML_cubicBezTo);
|
||||
writePoint(rPoints[nPoint]);
|
||||
writePoint(rPoints[nPoint + 1]);
|
||||
writePoint(bClosing ? rPoints[0] : rPoints[nPoint + 2]);
|
||||
mpFS->endElementNS(XML_a, XML_cubicBezTo);
|
||||
nPoint += 2;
|
||||
}
|
||||
|
||||
mpFS->singleElementNS(XML_a, XML_close);
|
||||
}
|
||||
|
||||
mpFS->endElementNS(XML_a, XML_path);
|
||||
mpFS->endElementNS(XML_a, XML_pathLst);
|
||||
mpFS->endElementNS(XML_a, XML_custGeom);
|
||||
return true;
|
||||
}
|
||||
|
||||
// version for SdrPathObj
|
||||
void DrawingML::WritePolyPolygon(const css::uno::Reference<css::drawing::XShape>& rXShape,
|
||||
const bool bClosed)
|
||||
|
||||
@@ -263,6 +263,7 @@ GradientName
|
||||
HatchName
|
||||
Graphic
|
||||
GraphicBitmap
|
||||
GraphicClipPolyPolygon
|
||||
GraphicColorMode
|
||||
GraphicCrop
|
||||
GraphicSize
|
||||
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -2418,6 +2418,38 @@ CPPUNIT_TEST_FIXTURE(SdOOXMLExportTest4, testSlideSectionsODPToPPTX)
|
||||
}
|
||||
}
|
||||
|
||||
CPPUNIT_TEST_FIXTURE(SdOOXMLExportTest4, testCool16078_placeholderKeepsItsOutline)
|
||||
{
|
||||
// Given a layout whose picture placeholder is clipped to an outline of two <a:path> elements:
|
||||
// a frame with two holes, one of them round, and a second path holding a triangle that crosses
|
||||
// the round one:
|
||||
createSdImpressDoc("pptx/custgeom-placeholder.pptx");
|
||||
save(TestFilter::PPTX);
|
||||
|
||||
// The outline is written back as the placeholder's own geometry. Without it the placeholder
|
||||
// saved as a plain rectangle, and its fill then hid both holes and what showed through.
|
||||
xmlDocUniquePtr pLayout = parseExportedLayoutNamed(u"Picture placeholder, clipped outline");
|
||||
OString aPath
|
||||
= "//p:sp[p:nvSpPr/p:nvPr/p:ph/@type='pic']/p:spPr/a:custGeom/a:pathLst/a:path"_ostr;
|
||||
assertXPath(pLayout, aPath, 1);
|
||||
// Three contours in one path, which is what subtracts the holes from the frame - the same three
|
||||
// the ODF side writes. The second path is not one of them: it is filled rather than cut out, so
|
||||
// the triangle it holds took a piece of the round hole back instead of adding a contour.
|
||||
assertXPath(pLayout, aPath + "/a:moveTo", 3);
|
||||
assertXPath(pLayout, aPath + "/a:close", 3);
|
||||
// Four sides each for the two rectangles, the closing one included.
|
||||
assertXPath(pLayout, aPath + "/a:lnTo", 8);
|
||||
// What is left of the round hole is still curves, not a polygon flattened out of them.
|
||||
assertXPath(pLayout, aPath + "/a:cubicBezTo", 6);
|
||||
assertXPath(pLayout, aPath + "/a:cubicBezTo[1]/a:pt", 3);
|
||||
// The frame's own corner, in the path's coordinates.
|
||||
assertXPath(pLayout, aPath + "/a:moveTo[1]/a:pt", "x", u"0");
|
||||
assertXPath(pLayout, aPath + "/a:moveTo[1]/a:pt", "y", u"0");
|
||||
|
||||
// The preset rectangle it used to write would have claimed the whole frame.
|
||||
assertXPath(pLayout, "//p:sp[p:nvSpPr/p:nvPr/p:ph/@type='pic']/p:spPr/a:prstGeom", 0);
|
||||
}
|
||||
|
||||
CPPUNIT_PLUGIN_IMPLEMENT();
|
||||
|
||||
/* vim:set shiftwidth=4 softtabstop=4 expandtab: */
|
||||
|
||||
@@ -136,6 +136,29 @@ CPPUNIT_TEST_FIXTURE(SdExportTest2, testClickActionSoundLinkDetected)
|
||||
CPPUNIT_ASSERT(lcl_hasSoundLink(*getSdDocShell()->GetDoc()));
|
||||
}
|
||||
|
||||
CPPUNIT_TEST_FIXTURE(SdExportTest2, testCool16078_placeholderOutlineToODF)
|
||||
{
|
||||
// Given the same outline the OOXML export test uses - a frame with two holes, one of them
|
||||
// round, and a second <a:path> holding a triangle that crosses the round one:
|
||||
createSdImpressDoc("pptx/custgeom-placeholder.pptx");
|
||||
save(TestFilter::ODP);
|
||||
|
||||
// ODF states it as a child of the frame, so that a placeholder keeps its outline here too.
|
||||
xmlDocUniquePtr pXmlDoc = parseExport(u"content.xml"_ustr);
|
||||
static constexpr OString aPath
|
||||
= "//draw:frame[@presentation:class='graphic']/loext:graphic-clip-poly"_ostr;
|
||||
assertXPath(pXmlDoc, aPath, 1);
|
||||
|
||||
// Three contours, the same three the OOXML side writes: the second path adds none of its own,
|
||||
// its triangle taking a piece of the round hole back instead. A contour begins at each M of the
|
||||
// svg:d.
|
||||
const OUString aOutline = getXPath(pXmlDoc, aPath, "d");
|
||||
CPPUNIT_ASSERT_EQUAL(sal_Int32(3),
|
||||
aOutline.getLength() - aOutline.replaceAll(u"M", u"").getLength());
|
||||
// The frame's own corner, where the outline starts.
|
||||
CPPUNIT_ASSERT(aOutline.startsWith("M0 0"));
|
||||
}
|
||||
|
||||
CPPUNIT_TEST_FIXTURE(SdExportTest2, testSoundLinkAllowedPerLink)
|
||||
{
|
||||
// Allowing one external transition sound through link management marks that
|
||||
|
||||
@@ -8,6 +8,8 @@
|
||||
*/
|
||||
|
||||
#include <sdpage.hxx>
|
||||
#include <View.hxx>
|
||||
#include <ViewShell.hxx>
|
||||
|
||||
#include "sdmodeltestbase.hxx"
|
||||
#include <tools/color.hxx>
|
||||
@@ -24,6 +26,9 @@
|
||||
#include <svx/xlineit0.hxx>
|
||||
#include <svx/xlnclit.hxx>
|
||||
#include <svx/sdooitm.hxx>
|
||||
#include <svx/svdobj.hxx>
|
||||
#include <svx/svdpagv.hxx>
|
||||
#include <svx/sdrhittesthelper.hxx>
|
||||
|
||||
#include <com/sun/star/awt/XBitmap.hpp>
|
||||
#include <com/sun/star/awt/Gradient.hpp>
|
||||
@@ -1432,6 +1437,170 @@ CPPUNIT_TEST_FIXTURE(SdImportTest2, testCool16078_clipPolygonBeforeTheImage)
|
||||
CPPUNIT_ASSERT_EQUAL(sal_Int32(4), aClip.Coordinates[0].getLength());
|
||||
}
|
||||
|
||||
CPPUNIT_TEST_FIXTURE(SdImportTest2, testCool16078_aPathThatPaintsNothingClipsNothing)
|
||||
{
|
||||
// Given a picture placeholder clipped to two paths that do not touch, the right one stating
|
||||
// fill="none":
|
||||
createSdImpressDoc("pptx/custgeom-nofill-path.pptx");
|
||||
|
||||
// Only the left one bounds anything. PowerPoint leaves a path that paints nothing out of the
|
||||
// clip, which three renders of one picture agree on - the path plain, fill="none", and
|
||||
// stroke="0" - so the polygon holds that one contour.
|
||||
drawing::PolyPolygonBezierCoords aClip;
|
||||
CPPUNIT_ASSERT(getShapeFromPage(0, 0)->getPropertyValue(u"GraphicClipPolyPolygon"_ustr)
|
||||
>>= aClip);
|
||||
CPPUNIT_ASSERT_EQUAL(sal_Int32(1), aClip.Coordinates.getLength());
|
||||
|
||||
// And the area the nofill path covers is not picked, while the other one's is. The paths span
|
||||
// 0..3000000 and 6000000..9000000 of a 9000000 x 5000000 space, which is 0..8333 and
|
||||
// 16667..25000 in 1/100 mm.
|
||||
auto xPlaceholder = getShapeFromPage(0, 0).queryThrow<drawing::XShape>();
|
||||
SdrObject* pPlaceholder = SdrObject::getSdrObjectFromXShape(xPlaceholder);
|
||||
CPPUNIT_ASSERT(pPlaceholder);
|
||||
SdrPageView* pPageView = getSdDocShell()->GetViewShell()->GetView()->GetSdrPageView();
|
||||
CPPUNIT_ASSERT(pPageView);
|
||||
const Point aTopLeft(pPlaceholder->GetSnapRect().TopLeft());
|
||||
CPPUNIT_ASSERT_MESSAGE("the path that paints nothing clipped the picture in",
|
||||
SdrObjectPrimitiveHit(*pPlaceholder, aTopLeft + Point(21000, 6000), {},
|
||||
*pPageView, nullptr, false)
|
||||
== nullptr);
|
||||
CPPUNIT_ASSERT_MESSAGE("the path that paints stopped being picked",
|
||||
SdrObjectPrimitiveHit(*pPlaceholder, aTopLeft + Point(4000, 6000), {},
|
||||
*pPageView, nullptr, false)
|
||||
!= nullptr);
|
||||
}
|
||||
|
||||
CPPUNIT_TEST_FIXTURE(SdImportTest2, testCool16078_overlappingContoursLeaveAHole)
|
||||
{
|
||||
// Given a placeholder clipped to two contours of one <a:path>, overlapping and wound the same
|
||||
// way, over a marker that sits in the overlap:
|
||||
createSdImpressDoc("pptx/custgeom-overlapping-contours.pptx");
|
||||
|
||||
// The contours of one path are filled by the even-odd rule, so two turns cancel: their overlap
|
||||
// is a hole, and a click there passes through the placeholder as it does in PowerPoint. Asking
|
||||
// where a click lands walks the primitives the shape paints, so the mask clipping them answers
|
||||
// here just as it does on screen.
|
||||
auto checkHole = [this](const OString& rWhen) {
|
||||
auto xPlaceholder = getShapeFromPage(0, 0).queryThrow<drawing::XShape>();
|
||||
SdrObject* pPlaceholder = SdrObject::getSdrObjectFromXShape(xPlaceholder);
|
||||
CPPUNIT_ASSERT(pPlaceholder);
|
||||
SdrPageView* pPageView = getSdDocShell()->GetViewShell()->GetView()->GetSdrPageView();
|
||||
CPPUNIT_ASSERT(pPageView);
|
||||
const Point aTopLeft(pPlaceholder->GetSnapRect().TopLeft());
|
||||
auto picksAt = [pPlaceholder, pPageView, &aTopLeft](tools::Long nX, tools::Long nY) {
|
||||
// No tolerance, so the areas answer and the outline drawn around them does not.
|
||||
return SdrObjectPrimitiveHit(*pPlaceholder, aTopLeft + Point(nX, nY), {}, *pPageView,
|
||||
nullptr, false)
|
||||
!= nullptr;
|
||||
};
|
||||
const OString aFirst = rWhen + ", the first contour alone";
|
||||
const OString aSecond = rWhen + ", the second contour alone";
|
||||
const OString aOverlap = rWhen + ", the overlap of the two";
|
||||
CPPUNIT_ASSERT_MESSAGE(aFirst.getStr(), picksAt(5000, 4000));
|
||||
CPPUNIT_ASSERT_MESSAGE(aSecond.getStr(), picksAt(18000, 11000));
|
||||
CPPUNIT_ASSERT_MESSAGE(aOverlap.getStr(), !picksAt(11000, 8000));
|
||||
};
|
||||
|
||||
checkHole("as imported"_ostr);
|
||||
|
||||
// Saving states the area the two contours leave, which no longer needs a rule to be read: a
|
||||
// hole is a contour of its own by then, and both formats carry it as one.
|
||||
saveAndReload(TestFilter::PPTX);
|
||||
checkHole("after a PPTX round-trip"_ostr);
|
||||
|
||||
saveAndReload(TestFilter::ODP);
|
||||
checkHole("after an ODP round-trip"_ostr);
|
||||
}
|
||||
|
||||
CPPUNIT_TEST_FIXTURE(SdImportTest2, testCool16078_placeholderKeepsItsOutline)
|
||||
{
|
||||
// Given a slide whose picture placeholder inherits, from its layout, an outline of two
|
||||
// <a:path> elements: a frame with two holes, one of them round, and a second path holding a
|
||||
// triangle that crosses the round one:
|
||||
createSdImpressDoc("pptx/custgeom-placeholder.pptx");
|
||||
|
||||
// The outline arrives as the shape's clip polygon, in the shape's own coordinates and 1/100 mm,
|
||||
// and covers the same area whichever format it has just been through. Dropping it left a plain
|
||||
// rectangle, whose fill then covered the slide behind it.
|
||||
auto checkOutline = [this](const OString& rWhen) {
|
||||
drawing::PolyPolygonBezierCoords aClip;
|
||||
CPPUNIT_ASSERT(getShapeFromPage(0, 0)->getPropertyValue(u"GraphicClipPolyPolygon"_ustr)
|
||||
>>= aClip);
|
||||
|
||||
// The frame is 9000000 x 5000000 EMU and the path spans the same, so a point maps to
|
||||
// 1/100 mm by EMU alone: 9000000 becomes 25000, 5000000 becomes 13889. Each contour is
|
||||
// closed, so it states its first point again at the end.
|
||||
CPPUNIT_ASSERT_EQUAL_MESSAGE(rWhen.getStr(), sal_Int32(3), aClip.Coordinates.getLength());
|
||||
const awt::Point aExpected[2][5] = {
|
||||
{ { 0, 0 }, { 25000, 0 }, { 25000, 13889 }, { 0, 13889 }, { 0, 0 } },
|
||||
{ { 1111, 8889 }, { 1111, 13056 }, { 9722, 13056 }, { 9722, 8889 }, { 1111, 8889 } }
|
||||
};
|
||||
for (sal_Int32 nContour = 0; nContour < 2; ++nContour)
|
||||
{
|
||||
const OString aWhich = rWhen + ", contour " + OString::number(nContour);
|
||||
CPPUNIT_ASSERT_EQUAL_MESSAGE(aWhich.getStr(), sal_Int32(5),
|
||||
aClip.Coordinates[nContour].getLength());
|
||||
for (sal_Int32 nPoint = 0; nPoint < 5; ++nPoint)
|
||||
{
|
||||
const OString aWhere = aWhich + ", point " + OString::number(nPoint);
|
||||
CPPUNIT_ASSERT_EQUAL_MESSAGE(aWhere.getStr(), aExpected[nContour][nPoint].X,
|
||||
aClip.Coordinates[nContour][nPoint].X);
|
||||
CPPUNIT_ASSERT_EQUAL_MESSAGE(aWhere.getStr(), aExpected[nContour][nPoint].Y,
|
||||
aClip.Coordinates[nContour][nPoint].Y);
|
||||
}
|
||||
}
|
||||
|
||||
// What the third contour is worth saying about is the area it leaves, so the shape is asked
|
||||
// where a click reaches it: the hit test walks the primitives it paints, so the mask
|
||||
// clipping them answers here just as it does on screen.
|
||||
auto xPlaceholder = getShapeFromPage(0, 0).queryThrow<drawing::XShape>();
|
||||
SdrObject* pPlaceholder = SdrObject::getSdrObjectFromXShape(xPlaceholder);
|
||||
CPPUNIT_ASSERT(pPlaceholder);
|
||||
SdrPageView* pPageView = getSdDocShell()->GetViewShell()->GetView()->GetSdrPageView();
|
||||
CPPUNIT_ASSERT(pPageView);
|
||||
const Point aTopLeft(pPlaceholder->GetSnapRect().TopLeft());
|
||||
struct Probe
|
||||
{
|
||||
std::string_view aWhat;
|
||||
Point aAt;
|
||||
bool bPicked;
|
||||
};
|
||||
const Probe aProbes[] = {
|
||||
{ "the frame itself", { 2000, 2000 }, true },
|
||||
{ "the rectangular hole", { 5400, 11000 }, false },
|
||||
// The round hole is a hole above the triangle and covered below it: a path of its own
|
||||
// is filled rather than cut out, so it takes a piece of that hole back. Reading the two
|
||||
// paths as one polygon subtracted the triangle instead, and the hole grew.
|
||||
{ "the round hole, above the triangle", { 17500, 7500 }, false },
|
||||
{ "the round hole, inside the triangle", { 17500, 11500 }, true },
|
||||
};
|
||||
for (const Probe& rProbe : aProbes)
|
||||
{
|
||||
const OString aWhere = rWhen + ", " + OString(rProbe.aWhat.data(), rProbe.aWhat.size());
|
||||
// No tolerance, so the areas answer and the outline drawn around them does not.
|
||||
CPPUNIT_ASSERT_EQUAL_MESSAGE(aWhere.getStr(), rProbe.bPicked,
|
||||
SdrObjectPrimitiveHit(*pPlaceholder, aTopLeft + rProbe.aAt,
|
||||
{}, *pPageView, nullptr, false)
|
||||
!= nullptr);
|
||||
}
|
||||
};
|
||||
|
||||
checkOutline("as imported"_ostr);
|
||||
|
||||
// Saving states the outline on the slide as well as on its layout, so reading the file back
|
||||
// gives the shape a geometry of its own where it used to have only the one it inherits. The
|
||||
// shape's own has to replace that, not join it: two geometries at once are not one this reads,
|
||||
// and the placeholder came back unclipped - PowerPoint meanwhile showed the very file
|
||||
// correctly.
|
||||
saveAndReload(TestFilter::PPTX);
|
||||
checkOutline("after a PPTX round-trip"_ostr);
|
||||
|
||||
// ODF carries it as a loext:graphic-clip-poly child of the frame, and has the same to answer
|
||||
// for: the same outline, from the same document, through the other format.
|
||||
saveAndReload(TestFilter::ODP);
|
||||
checkOutline("after an ODP round-trip"_ostr);
|
||||
}
|
||||
|
||||
CPPUNIT_PLUGIN_IMPLEMENT();
|
||||
|
||||
/* vim:set shiftwidth=4 softtabstop=4 expandtab: */
|
||||
|
||||
@@ -2752,7 +2752,10 @@ ShapeExport& PowerPointShapeExport::WritePlaceholderShape(const Reference< XShap
|
||||
// inheritance.
|
||||
mpFS->startElementNS(XML_p, XML_spPr);
|
||||
WriteShapeTransformation(xShape, XML_a);
|
||||
WritePresetShape("rect"_ostr);
|
||||
// A picture placeholder can be clipped to an outline, which is the shape's own geometry in the
|
||||
// file; where there is one it takes the place of the rectangle.
|
||||
if (!WriteGraphicClipCustomGeometry(xProps, xShape->getSize()))
|
||||
WritePresetShape("rect"_ostr);
|
||||
if (xProps.is())
|
||||
{
|
||||
// A picture placeholder's "Graphic" property is the internal
|
||||
|
||||
Reference in New Issue
Block a user