Skip to content
Permalink
Browse files
[symbology] Add support for rendering line pattern fills line-by-line
When exporting to a vector format (e.g. PDF) or when a line subsymbol
has dynamic (data defined properties), automatically switch to
a line-by-line based approach for rendering the fill instead of the
previous raster tiled pattern based approach.

While it's slower to render (not noticable for desktop users, but
likely enough to affect server deployments), this has many benefits:

1. Smaller PDF/SVG output file sizes, since the fills aren't rasterized
2. PDF/SVG files which are easier to modify in external apps for
post production, as each individual line in the pattern can be
modified.
3. Better quality PDF/SVG outputs, since the fill isn't DPI
dependant and looks awesome regardless of how close in you zoom
4. No visible artefacts at certain angles/distances/line symbol
styles

And even more excitingly, it opens the door for a range of
new symbol styles, eg.

- line patterns where the individual lines change color/width/dash/...
- line patterns with marker line symbols on center point/etc
- geometry generator effects per line, e.g. wavy line patterns, hand
drawn line styles, etc

Sponsored by North Road, thanks to SLYR

Fixes #16100
  • Loading branch information
nyalldawson committed Oct 25, 2021
1 parent 03e304f commit 088ffe6a460b96bd525fa82a1542b5f6a2c7fe5f
@@ -1535,6 +1535,8 @@ ownership of the returned object.

virtual void stopRender( QgsSymbolRenderContext &context );

virtual void renderPolygon( const QPolygonF &points, const QVector<QPolygonF> *rings, QgsSymbolRenderContext &context );

virtual QVariantMap properties() const;

virtual QgsLinePatternFillSymbolLayer *clone() const /Factory/;
@@ -1765,6 +1767,10 @@ Returns the map unit scale for the pattern's line offset.

virtual bool hasDataDefinedProperties() const;

virtual void startFeatureRender( const QgsFeature &feature, QgsRenderContext &context );

virtual void stopFeatureRender( const QgsFeature &feature, QgsRenderContext &context );


protected:

@@ -2497,10 +2497,7 @@ QgsLinePatternFillSymbolLayer::QgsLinePatternFillSymbolLayer()
QgsImageFillSymbolLayer::setSubSymbol( nullptr ); //no stroke
}

QgsLinePatternFillSymbolLayer::~QgsLinePatternFillSymbolLayer()
{
delete mFillLineSymbol;
}
QgsLinePatternFillSymbolLayer::~QgsLinePatternFillSymbolLayer() = default;

void QgsLinePatternFillSymbolLayer::setLineWidth( double w )
{
@@ -2528,22 +2525,16 @@ bool QgsLinePatternFillSymbolLayer::setSubSymbol( QgsSymbol *symbol )

if ( symbol->type() == Qgis::SymbolType::Line )
{
QgsLineSymbol *lineSymbol = dynamic_cast<QgsLineSymbol *>( symbol );
if ( lineSymbol )
{
delete mFillLineSymbol;
mFillLineSymbol = lineSymbol;

return true;
}
mFillLineSymbol.reset( qgis::down_cast<QgsLineSymbol *>( symbol ) );
return true;
}
delete symbol;
return false;
}

QgsSymbol *QgsLinePatternFillSymbolLayer::subSymbol()
{
return mFillLineSymbol;
return mFillLineSymbol.get();
}

QSet<QString> QgsLinePatternFillSymbolLayer::usedAttributes( const QgsRenderContext &context ) const
@@ -2563,6 +2554,16 @@ bool QgsLinePatternFillSymbolLayer::hasDataDefinedProperties() const
return false;
}

void QgsLinePatternFillSymbolLayer::startFeatureRender( const QgsFeature &, QgsRenderContext & )
{
// deliberately don't pass this on to subsymbol here
}

void QgsLinePatternFillSymbolLayer::stopFeatureRender( const QgsFeature &, QgsRenderContext & )
{
// deliberately don't pass this on to subsymbol here
}

double QgsLinePatternFillSymbolLayer::estimateMaxBleed( const QgsRenderContext & ) const
{
return 0;
@@ -3017,22 +3018,178 @@ void QgsLinePatternFillSymbolLayer::applyPattern( const QgsSymbolRenderContext &

void QgsLinePatternFillSymbolLayer::startRender( QgsSymbolRenderContext &context )
{
applyPattern( context, mBrush, mLineAngle, mDistance );
// if we are using a vector based output, we need to render points as vectors
// (OR if the marker has data defined symbology, in which case we need to evaluate this point-by-point)
mRenderUsingLines = context.renderContext().forceVectorOutput()
|| mFillLineSymbol->hasDataDefinedProperties();

if ( mFillLineSymbol )
if ( mRenderUsingLines )
{
mFillLineSymbol->startRender( context.renderContext(), context.fields() );
if ( mFillLineSymbol )
mFillLineSymbol->startRender( context.renderContext(), context.fields() );
}
else
{
// optimised render for screen only, use image based brush
applyPattern( context, mBrush, mLineAngle, mDistance );
}
}

void QgsLinePatternFillSymbolLayer::stopRender( QgsSymbolRenderContext &context )
{
if ( mFillLineSymbol )
if ( mRenderUsingLines && mFillLineSymbol )
{
mFillLineSymbol->stopRender( context.renderContext() );
}
}

void QgsLinePatternFillSymbolLayer::renderPolygon( const QPolygonF &points, const QVector<QPolygonF> *rings, QgsSymbolRenderContext &context )
{
if ( !mRenderUsingLines )
{
// use image based brush for speed
QgsImageFillSymbolLayer::renderPolygon( points, rings, context );
return;
}

// vector based output - so draw line by line!
QPainter *p = context.renderContext().painter();
if ( !p )
{
return;
}

double lineAngle = mLineAngle;
if ( mDataDefinedProperties.isActive( QgsSymbolLayer::PropertyLineAngle ) )
{
context.setOriginalValueVariable( mLineAngle );
lineAngle = mDataDefinedProperties.valueAsDouble( QgsSymbolLayer::PropertyLineAngle, context.renderContext().expressionContext(), mLineAngle );
}

double distance = mDistance;
if ( mDataDefinedProperties.isActive( QgsSymbolLayer::PropertyLineDistance ) )
{
context.setOriginalValueVariable( mDistance );
distance = mDataDefinedProperties.valueAsDouble( QgsSymbolLayer::PropertyLineDistance, context.renderContext().expressionContext(), mDistance );
}
const double outputPixelDistance = context.renderContext().convertToPainterUnits( distance, mDistanceUnit, mDistanceMapUnitScale );

double offset = mOffset;
if ( mDataDefinedProperties.isActive( QgsSymbolLayer::PropertyLineDistance ) )
{
context.setOriginalValueVariable( mDistance );
distance = mDataDefinedProperties.valueAsDouble( QgsSymbolLayer::PropertyLineDistance, context.renderContext().expressionContext(), mDistance );
}
double outputPixelOffset = context.renderContext().convertToPainterUnits( offset, mOffsetUnit, mOffsetMapUnitScale );

// fix truncated pattern with larger offsets
outputPixelOffset = std::fmod( outputPixelOffset, outputPixelDistance );
if ( outputPixelOffset > outputPixelDistance / 2.0 )
outputPixelOffset -= outputPixelDistance;

p->setPen( QPen( Qt::NoPen ) );

if ( context.selected() )
{
QColor selColor = context.renderContext().selectionColor();
p->setBrush( QBrush( selColor ) );
_renderPolygon( p, points, rings, context );
}

// if invalid parameters, skip out
if ( qgsDoubleNear( distance, 0 ) )
return;

p->save();

QPainterPath path;
path.addPolygon( points );
if ( rings )
{
for ( const QPolygonF &ring : *rings )
{
path.addPolygon( ring );
}
}
p->setClipPath( path, Qt::IntersectClip );

const bool applyBrushTransform = applyBrushTransformFromContext( &context );
const QRectF boundingRect = points.boundingRect();

QTransform invertedRotateTransform;
double left;
double top;
double right;
double bottom;

QTransform transform;
if ( applyBrushTransform )
{
// rotation applies around center of feature
transform.translate( -boundingRect.center().x(),
-boundingRect.center().y() );
transform.rotate( lineAngle );
transform.translate( boundingRect.center().x(),
boundingRect.center().y() );
}
else
{
// rotation applies around top of viewport
transform.rotate( lineAngle );
}

const QRectF transformedBounds = transform.map( points ).boundingRect();

// bounds are expanded out a bit to account for maximum line width
const double buffer = QgsSymbolLayerUtils::estimateMaxSymbolBleed( mFillLineSymbol.get(), context.renderContext() );
left = transformedBounds.left() - buffer * 2;
top = transformedBounds.top() - buffer * 2;
right = transformedBounds.right() + buffer * 2;
bottom = transformedBounds.bottom() + buffer * 2;
invertedRotateTransform = transform.inverted();

if ( !applyBrushTransform )
{
top -= transformedBounds.top() - ( outputPixelDistance * std::floor( transformedBounds.top() / outputPixelDistance ) );
}

QgsExpressionContextScope *scope = new QgsExpressionContextScope();
QgsExpressionContextScopePopper scopePopper( context.renderContext().expressionContext(), scope );
const bool needsExpressionContext = mFillLineSymbol->hasDataDefinedProperties();

const bool prevIsSubsymbol = context.renderContext().flags() & Qgis::RenderContextFlag::RenderingSubSymbol;
context.renderContext().setFlag( Qgis::RenderContextFlag::RenderingSubSymbol );

int currentLine = 0;
for ( double currentY = top; currentY <= bottom; currentY += outputPixelDistance )
{
if ( context.renderContext().renderingStopped() )
break;

if ( needsExpressionContext )
scope->addVariable( QgsExpressionContextScope::StaticVariable( QStringLiteral( "symbol_line_number" ), ++currentLine, true ) );

double x1 = left;
double y1 = currentY;
double x2 = left;
double y2 = currentY;
invertedRotateTransform.map( left, currentY - outputPixelOffset, &x1, &y1 );
invertedRotateTransform.map( right, currentY - outputPixelOffset, &x2, &y2 );

// todo -- if we do proper intersects clipping, we may end up with multiline string here, so we'd need to wrap the
// rendering of each part with in
// mFillLineSymbol->startFeatureRender();
// ...
// mFillLineSymbol->stopFeatureRender();

mFillLineSymbol->renderPolyline( QPolygonF() << QPointF( x1, y1 ) << QPointF( x2, y2 ), context.feature(), context.renderContext() );
}

p->restore();

context.renderContext().setFlag( Qgis::RenderContextFlag::RenderingSubSymbol, prevIsSubsymbol );
}

QVariantMap QgsLinePatternFillSymbolLayer::properties() const
{
QVariantMap map = QgsImageFillSymbolLayer::properties();
@@ -3831,6 +3988,9 @@ void QgsPointPatternFillSymbolLayer::renderPolygon( const QPolygonF &points, con
int currentCol = -3; // because we actually render a few rows/cols outside the bounds, try to align the col/row numbers to start at 1 for the first visible row/col
for ( double currentX = left; currentX <= right; currentX += width, alternateColumn = !alternateColumn )
{
if ( context.renderContext().renderingStopped() )
break;

if ( needsExpressionContext )
scope->addVariable( QgsExpressionContextScope::StaticVariable( QStringLiteral( "symbol_marker_column" ), ++currentCol, true ) );

@@ -3839,6 +3999,9 @@ void QgsPointPatternFillSymbolLayer::renderPolygon( const QPolygonF &points, con
int currentRow = -3;
for ( double currentY = top; currentY <= bottom; currentY += height, alternateRow = !alternateRow )
{
if ( context.renderContext().renderingStopped() )
break;

double y = currentY + heightOffset;
double x = columnX;
if ( alternateRow )
@@ -1411,6 +1411,7 @@ class CORE_EXPORT QgsLinePatternFillSymbolLayer: public QgsImageFillSymbolLayer
QString layerType() const override;
void startRender( QgsSymbolRenderContext &context ) override;
void stopRender( QgsSymbolRenderContext &context ) override;
void renderPolygon( const QPolygonF &points, const QVector<QPolygonF> *rings, QgsSymbolRenderContext &context ) override;
QVariantMap properties() const override;
QgsLinePatternFillSymbolLayer *clone() const override SIP_FACTORY;
void toSld( QDomDocument &doc, QDomElement &element, const QVariantMap &props ) const override;
@@ -1603,6 +1604,8 @@ class CORE_EXPORT QgsLinePatternFillSymbolLayer: public QgsImageFillSymbolLayer
QgsSymbol *subSymbol() override;
QSet<QString> usedAttributes( const QgsRenderContext &context ) const override;
bool hasDataDefinedProperties() const override;
void startFeatureRender( const QgsFeature &feature, QgsRenderContext &context ) override;
void stopFeatureRender( const QgsFeature &feature, QgsRenderContext &context ) override;

protected:

@@ -1624,6 +1627,8 @@ class CORE_EXPORT QgsLinePatternFillSymbolLayer: public QgsImageFillSymbolLayer
QgsUnitTypes::RenderUnit mOffsetUnit = QgsUnitTypes::RenderMillimeters;
QgsMapUnitScale mOffsetMapUnitScale;

bool mRenderUsingLines = false;

#ifdef SIP_RUN
QgsLinePatternFillSymbolLayer( const QgsLinePatternFillSymbolLayer &other );
#endif
@@ -1632,7 +1637,7 @@ class CORE_EXPORT QgsLinePatternFillSymbolLayer: public QgsImageFillSymbolLayer
void applyPattern( const QgsSymbolRenderContext &context, QBrush &brush, double lineAngle, double distance );

//! Fill line
QgsLineSymbol *mFillLineSymbol = nullptr;
std::unique_ptr< QgsLineSymbol > mFillLineSymbol;
};

/**

0 comments on commit 088ffe6

Please sign in to comment.