1c902230fe
* Bulk replaced tabs with spaces * Bulk removed trailing whitespace from lines * Replaced c-style comments with c++-style comments in file banners * Replace nested namespace definitions with single concise definitions (C++17), this keeps the indentation more manageable * Cleanup ordering and spacing of include directives * Bulk renaming of header file extensions from '.h' to '.hpp'. * Update CODING-STYLE.md * Update target_compile_features from cxx_std_11 to cxx_std_20. * Refresh .clang-format file. Still needs a lot of tweaking.
630 lines
23 KiB
C++
630 lines
23 KiB
C++
// ModelImageObject.cpp
|
|
//
|
|
// Copyright (C) 2013-2016 Jaye Evins <evins@snaught.com>
|
|
//
|
|
// This file is part of gLabels-qt.
|
|
//
|
|
// gLabels-qt is free software: you can redistribute it and/or modify
|
|
// it under the terms of the GNU General Public License as published by
|
|
// the Free Software Foundation, either version 3 of the License, or
|
|
// (at your option) any later version.
|
|
//
|
|
// gLabels-qt is distributed in the hope that it will be useful,
|
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
// GNU General Public License for more details.
|
|
//
|
|
// You should have received a copy of the GNU General Public License
|
|
// along with gLabels-qt. If not, see <http://www.gnu.org/licenses/>.
|
|
//
|
|
|
|
|
|
#include "ModelImageObject.hpp"
|
|
|
|
#include "Model.hpp"
|
|
#include "Size.hpp"
|
|
|
|
#include <QBrush>
|
|
#include <QDebug>
|
|
#include <QDir>
|
|
#include <QFileInfo>
|
|
#include <QImage>
|
|
#include <QPen>
|
|
|
|
|
|
//
|
|
// Private
|
|
//
|
|
namespace
|
|
{
|
|
using namespace glabels::model;
|
|
|
|
const QColor fillColor = QColor( 224, 224, 224, 255 );
|
|
const QColor labelColor = QColor( 102, 102, 102, 255 );
|
|
const Distance pad = Distance::pt(2);
|
|
}
|
|
|
|
|
|
namespace glabels::model
|
|
{
|
|
|
|
///
|
|
/// Static data
|
|
///
|
|
QImage ModelImageObject::smDefaultImage( ":images/checkerboard.png" );
|
|
|
|
|
|
///
|
|
/// Constructor
|
|
///
|
|
ModelImageObject::ModelImageObject()
|
|
{
|
|
mOutline.setOwner( this );
|
|
|
|
mHandles.push_back( Handle( this, Handle::NW ) );
|
|
mHandles.push_back( Handle( this, Handle::N ) );
|
|
mHandles.push_back( Handle( this, Handle::NE ) );
|
|
mHandles.push_back( Handle( this, Handle::E ) );
|
|
mHandles.push_back( Handle( this, Handle::SE ) );
|
|
mHandles.push_back( Handle( this, Handle::S ) );
|
|
mHandles.push_back( Handle( this, Handle::SW ) );
|
|
mHandles.push_back( Handle( this, Handle::W ) );
|
|
|
|
mLockAspectRatio = true;
|
|
}
|
|
|
|
|
|
///
|
|
/// Constructor
|
|
///
|
|
ModelImageObject::ModelImageObject( Distance x0,
|
|
Distance y0,
|
|
Distance w,
|
|
Distance h,
|
|
bool lockAspectRatio,
|
|
const TextNode& filenameNode,
|
|
const QTransform& matrix,
|
|
bool shadowState,
|
|
Distance shadowX,
|
|
Distance shadowY,
|
|
double shadowOpacity,
|
|
const ColorNode& shadowColorNode )
|
|
: ModelObject( x0,
|
|
y0,
|
|
w,
|
|
h,
|
|
lockAspectRatio,
|
|
matrix,
|
|
shadowState,
|
|
shadowX,
|
|
shadowY,
|
|
shadowOpacity,
|
|
shadowColorNode )
|
|
{
|
|
mOutline.setOwner( this );
|
|
|
|
mHandles.push_back( Handle( this, Handle::NW ) );
|
|
mHandles.push_back( Handle( this, Handle::N ) );
|
|
mHandles.push_back( Handle( this, Handle::NE ) );
|
|
mHandles.push_back( Handle( this, Handle::E ) );
|
|
mHandles.push_back( Handle( this, Handle::SE ) );
|
|
mHandles.push_back( Handle( this, Handle::S ) );
|
|
mHandles.push_back( Handle( this, Handle::SW ) );
|
|
mHandles.push_back( Handle( this, Handle::W ) );
|
|
|
|
mFilenameNode = filenameNode;
|
|
|
|
loadImage();
|
|
}
|
|
|
|
|
|
///
|
|
/// Constructor
|
|
///
|
|
ModelImageObject::ModelImageObject( Distance x0,
|
|
Distance y0,
|
|
Distance w,
|
|
Distance h,
|
|
bool lockAspectRatio,
|
|
const QString& filename,
|
|
const QImage& image,
|
|
const QTransform& matrix,
|
|
bool shadowState,
|
|
Distance shadowX,
|
|
Distance shadowY,
|
|
double shadowOpacity,
|
|
const ColorNode& shadowColorNode )
|
|
: ModelObject( x0,
|
|
y0,
|
|
w,
|
|
h,
|
|
lockAspectRatio,
|
|
matrix,
|
|
shadowState,
|
|
shadowX,
|
|
shadowY,
|
|
shadowOpacity,
|
|
shadowColorNode )
|
|
{
|
|
mOutline.setOwner( this );
|
|
|
|
mHandles.push_back( Handle( this, Handle::NW ) );
|
|
mHandles.push_back( Handle( this, Handle::N ) );
|
|
mHandles.push_back( Handle( this, Handle::NE ) );
|
|
mHandles.push_back( Handle( this, Handle::E ) );
|
|
mHandles.push_back( Handle( this, Handle::SE ) );
|
|
mHandles.push_back( Handle( this, Handle::S ) );
|
|
mHandles.push_back( Handle( this, Handle::SW ) );
|
|
mHandles.push_back( Handle( this, Handle::W ) );
|
|
|
|
mImage = image;
|
|
mFilenameNode = TextNode( false, filename );
|
|
}
|
|
|
|
|
|
///
|
|
/// Constructor
|
|
///
|
|
ModelImageObject::ModelImageObject( Distance x0,
|
|
Distance y0,
|
|
Distance w,
|
|
Distance h,
|
|
bool lockAspectRatio,
|
|
const QString& filename,
|
|
const QByteArray& svg,
|
|
const QTransform& matrix,
|
|
bool shadowState,
|
|
Distance shadowX,
|
|
Distance shadowY,
|
|
double shadowOpacity,
|
|
const ColorNode& shadowColorNode )
|
|
: ModelObject( x0,
|
|
y0,
|
|
w,
|
|
h,
|
|
lockAspectRatio,
|
|
matrix,
|
|
shadowState,
|
|
shadowX,
|
|
shadowY,
|
|
shadowOpacity,
|
|
shadowColorNode )
|
|
{
|
|
mOutline.setOwner( this );
|
|
|
|
mHandles.push_back( Handle( this, Handle::NW ) );
|
|
mHandles.push_back( Handle( this, Handle::N ) );
|
|
mHandles.push_back( Handle( this, Handle::NE ) );
|
|
mHandles.push_back( Handle( this, Handle::E ) );
|
|
mHandles.push_back( Handle( this, Handle::SE ) );
|
|
mHandles.push_back( Handle( this, Handle::S ) );
|
|
mHandles.push_back( Handle( this, Handle::SW ) );
|
|
mHandles.push_back( Handle( this, Handle::W ) );
|
|
|
|
if ( QSvgRenderer( svg ).isValid() )
|
|
{
|
|
mSvg = svg;
|
|
}
|
|
|
|
mFilenameNode = TextNode( false, filename );
|
|
}
|
|
|
|
|
|
///
|
|
/// Copy constructor
|
|
///
|
|
ModelImageObject::ModelImageObject( const ModelImageObject* object ) : ModelObject(object)
|
|
{
|
|
mFilenameNode = object->mFilenameNode;
|
|
mImage = object->mImage;
|
|
mSvg = object->mSvg;
|
|
}
|
|
|
|
|
|
///
|
|
/// Clone
|
|
///
|
|
ModelImageObject* ModelImageObject::clone() const
|
|
{
|
|
return new ModelImageObject( this );
|
|
}
|
|
|
|
|
|
///
|
|
/// Image filenameNode Property Getter
|
|
///
|
|
TextNode ModelImageObject::filenameNode() const
|
|
{
|
|
return mFilenameNode;
|
|
}
|
|
|
|
|
|
///
|
|
/// Image filenameNode Property Setter
|
|
///
|
|
void ModelImageObject::setFilenameNode( const TextNode& value )
|
|
{
|
|
if ( mFilenameNode != value )
|
|
{
|
|
mFilenameNode = value;
|
|
loadImage();
|
|
|
|
emit changed();
|
|
}
|
|
}
|
|
|
|
|
|
///
|
|
/// Image image Property Getter
|
|
///
|
|
const QImage& ModelImageObject::image() const
|
|
{
|
|
return mImage;
|
|
}
|
|
|
|
|
|
///
|
|
/// Image Property Setter
|
|
///
|
|
void ModelImageObject::setImage( const QImage& value )
|
|
{
|
|
if ( !value.isNull() )
|
|
{
|
|
mSvg.clear();
|
|
|
|
mImage = value;
|
|
quint16 cs = qChecksum( QByteArray( (const char*)mImage.constBits(), mImage.sizeInBytes() ) );
|
|
mFilenameNode = TextNode( false, QString("%image_%1%").arg( cs ) );
|
|
|
|
emit changed();
|
|
}
|
|
}
|
|
|
|
|
|
///
|
|
/// Image Property Setter
|
|
///
|
|
void ModelImageObject::setImage( const QString& name, const QImage& value )
|
|
{
|
|
if ( !value.isNull() )
|
|
{
|
|
mSvg.clear();
|
|
|
|
mImage = value;
|
|
mFilenameNode = TextNode( false, name );
|
|
|
|
emit changed();
|
|
}
|
|
}
|
|
|
|
|
|
///
|
|
/// Image svg Property Getter
|
|
///
|
|
const QByteArray& ModelImageObject::svg() const
|
|
{
|
|
return mSvg;
|
|
}
|
|
|
|
|
|
///
|
|
/// Image svgSource Property Setter
|
|
///
|
|
void ModelImageObject::setSvg( const QString& name, const QByteArray& value )
|
|
{
|
|
if ( !value.isEmpty() )
|
|
{
|
|
mImage = QImage(); // clear
|
|
mSvg.clear();
|
|
|
|
if ( QSvgRenderer( value ).isValid() )
|
|
{
|
|
mSvg = value;
|
|
}
|
|
mFilenameNode = TextNode( false, name );
|
|
|
|
emit changed();
|
|
}
|
|
}
|
|
|
|
|
|
///
|
|
/// naturalSize Property Getter (assumes 72 DPI, i.e. 1pixel == 1pt)
|
|
///
|
|
Size ModelImageObject::naturalSize() const
|
|
{
|
|
Size size( Distance::pt(72), Distance::pt(72) );
|
|
|
|
if ( !mImage.isNull() )
|
|
{
|
|
QSize qsize = mImage.size();
|
|
size.setW( Distance::pt( qsize.width() ) );
|
|
size.setH( Distance::pt( qsize.height() ) );
|
|
}
|
|
else if ( !mSvg.isEmpty() )
|
|
{
|
|
QSize qsize = QSvgRenderer( mSvg ).defaultSize();
|
|
size.setW( Distance::pt( qsize.width() ) );
|
|
size.setH( Distance::pt( qsize.height() ) );
|
|
}
|
|
|
|
return size;
|
|
}
|
|
|
|
|
|
///
|
|
/// Draw shadow of object
|
|
///
|
|
void ModelImageObject::drawShadow( QPainter* painter,
|
|
bool inEditor,
|
|
const merge::Record& record,
|
|
const Variables& variables ) const
|
|
{
|
|
QRectF destRect( 0, 0, mW.pt(), mH.pt() );
|
|
|
|
QColor shadowColor = mShadowColorNode.color( record, variables );
|
|
shadowColor.setAlphaF( mShadowOpacity );
|
|
|
|
if ( !mImage.isNull() && mImage.hasAlphaChannel() && (mImage.depth() == 32) )
|
|
{
|
|
auto shadowImage = createShadowImage( mImage, shadowColor );
|
|
painter->drawImage( destRect, shadowImage );
|
|
}
|
|
else if ( !mImage.isNull() || !mSvg.isEmpty() || inEditor )
|
|
{
|
|
painter->setBrush( shadowColor );
|
|
painter->setPen( QPen( Qt::NoPen ) );
|
|
|
|
painter->drawRect( destRect );
|
|
}
|
|
else
|
|
{
|
|
QString filename = mFilenameNode.text( record, variables ).trimmed();
|
|
QImage image;
|
|
QByteArray svg;
|
|
if ( readImageFile( filename, image, svg ) )
|
|
{
|
|
if ( !image.isNull() && image.hasAlphaChannel() && (image.depth() == 32) )
|
|
{
|
|
QImage shadowImage = createShadowImage( image, shadowColor );
|
|
painter->drawImage( destRect, shadowImage );
|
|
}
|
|
else
|
|
{
|
|
painter->setBrush( shadowColor );
|
|
painter->setPen( QPen( Qt::NoPen ) );
|
|
|
|
painter->drawRect( destRect );
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
///
|
|
/// Draw object itself
|
|
///
|
|
void ModelImageObject::drawObject( QPainter* painter,
|
|
bool inEditor,
|
|
const merge::Record& record,
|
|
const Variables& variables ) const
|
|
{
|
|
QRectF destRect( 0, 0, mW.pt(), mH.pt() );
|
|
|
|
if ( inEditor && (mFilenameNode.isField() || (mImage.isNull() && mSvg.isEmpty()) ) )
|
|
{
|
|
//
|
|
// Render default place holder image
|
|
//
|
|
painter->save();
|
|
painter->setRenderHint( QPainter::SmoothPixmapTransform, false );
|
|
painter->drawImage( destRect, smDefaultImage );
|
|
painter->restore();
|
|
|
|
//
|
|
// Print label on top of place holder image, if we have room
|
|
//
|
|
if ( (mW > 6*pad) && (mH > 4*pad) )
|
|
{
|
|
QString labelText = tr("No image");
|
|
if ( mFilenameNode.isField() )
|
|
{
|
|
labelText = QString( "${%1}" ).arg( mFilenameNode.data() );
|
|
}
|
|
|
|
// Determine font size for labelText
|
|
QFont font( "Sans" );
|
|
font.setPointSizeF( 6 );
|
|
|
|
QFontMetricsF fm( font );
|
|
QRectF textRect = fm.boundingRect( labelText );
|
|
|
|
double wPts = (mW - 2*pad).pt();
|
|
double hPts = (mH - 2*pad).pt();
|
|
if ( (wPts < textRect.width()) || (hPts < textRect.height()) )
|
|
{
|
|
double scaleX = wPts / textRect.width();
|
|
double scaleY = hPts / textRect.height();
|
|
font.setPointSizeF( 6 * std::min( scaleX, scaleY ) );
|
|
}
|
|
|
|
// Render hole for text (font size may have changed above)
|
|
fm = QFontMetricsF( font );
|
|
textRect = fm.boundingRect( labelText );
|
|
|
|
QRectF holeRect( (mW.pt() - textRect.width())/2 - pad.pt(),
|
|
(mH.pt() - textRect.height())/2 - pad.pt(),
|
|
textRect.width() + 2*pad.pt(),
|
|
textRect.height() + 2*pad.pt() );
|
|
|
|
painter->setPen( Qt::NoPen );
|
|
painter->setBrush( QBrush( fillColor ) );
|
|
painter->drawRect( holeRect );
|
|
|
|
// Render text
|
|
painter->setFont( font );
|
|
painter->setPen( QPen( labelColor ) );
|
|
painter->drawText( QRectF( 0, 0, mW.pt(), mH.pt() ),
|
|
Qt::AlignCenter,
|
|
labelText );
|
|
}
|
|
}
|
|
else if ( !mImage.isNull() )
|
|
{
|
|
painter->drawImage( destRect, mImage );
|
|
}
|
|
else if ( !mSvg.isEmpty() )
|
|
{
|
|
QSvgRenderer( mSvg ).render( painter, destRect );
|
|
}
|
|
else if ( mFilenameNode.isField() )
|
|
{
|
|
QString filename = mFilenameNode.text( record, variables ).trimmed();
|
|
QImage image;
|
|
QByteArray svg;
|
|
if ( readImageFile( filename, image, svg ) )
|
|
{
|
|
if ( !image.isNull() )
|
|
{
|
|
painter->drawImage( destRect, image );
|
|
}
|
|
else if ( !svg.isEmpty() )
|
|
{
|
|
QSvgRenderer( svg ).render( painter, destRect );
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
///
|
|
/// Path to test for hover condition
|
|
///
|
|
QPainterPath ModelImageObject::hoverPath( double scale ) const
|
|
{
|
|
QPainterPath path;
|
|
path.addRect( 0, 0, mW.pt(), mH.pt() );
|
|
|
|
return path;
|
|
}
|
|
|
|
|
|
///
|
|
/// Load image
|
|
///
|
|
void ModelImageObject::loadImage()
|
|
{
|
|
mImage = QImage(); // clear
|
|
mSvg.clear();
|
|
|
|
if ( !mFilenameNode.isField() )
|
|
{
|
|
QString filename = mFilenameNode.data();
|
|
if ( readImageFile( filename, mImage, mSvg ) )
|
|
{
|
|
double aspectRatio = 0;
|
|
if ( !mSvg.isEmpty() )
|
|
{
|
|
// Adjust size based on aspect ratio of SVG image
|
|
QRectF rect = QSvgRenderer( mSvg ).viewBoxF();
|
|
aspectRatio = rect.width() ? rect.height() / rect.width() : 0;
|
|
}
|
|
else
|
|
{
|
|
// Adjust size based on aspect ratio of image
|
|
double imageW = mImage.width();
|
|
double imageH = mImage.height();
|
|
aspectRatio = imageW ? imageH / imageW : 0;
|
|
}
|
|
|
|
if ( aspectRatio )
|
|
{
|
|
if ( mH > mW*aspectRatio )
|
|
{
|
|
mH = mW*aspectRatio;
|
|
}
|
|
else
|
|
{
|
|
mW = mH/aspectRatio;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
///
|
|
/// Read an image or svg file
|
|
///
|
|
bool ModelImageObject::readImageFile( const QString& fileName,
|
|
QImage& image,
|
|
QByteArray& svg ) const
|
|
{
|
|
image = QImage(); // clear
|
|
svg.clear();
|
|
|
|
if ( !fileName.isEmpty() )
|
|
{
|
|
QFileInfo fileInfo( fileName );
|
|
if ( fileInfo.isRelative() )
|
|
{
|
|
// Look for image file relative to project file 1st then CWD 2nd
|
|
auto* model = dynamic_cast<Model*>( parent() );
|
|
QDir::setSearchPaths( "images", {model ? model->dirPath() : "", QDir::currentPath()} );
|
|
fileInfo.setFile( QString("images:") + fileName );
|
|
}
|
|
|
|
if ( fileInfo.isReadable() )
|
|
{
|
|
if ( fileInfo.suffix().toLower() == "svg" )
|
|
{
|
|
QFile file( fileInfo.filePath() );
|
|
if ( file.open( QFile::ReadOnly ) )
|
|
{
|
|
svg = file.readAll();
|
|
file.close();
|
|
QSvgRenderer renderer( svg );
|
|
if ( !renderer.isValid() )
|
|
{
|
|
svg.clear();
|
|
}
|
|
}
|
|
}
|
|
else
|
|
{
|
|
image = QImage( fileInfo.filePath() );
|
|
}
|
|
}
|
|
}
|
|
|
|
return !image.isNull() || !svg.isEmpty();
|
|
}
|
|
|
|
|
|
///
|
|
/// Create shadow image
|
|
///
|
|
QImage ModelImageObject::createShadowImage( const QImage& image,
|
|
const QColor& color ) const
|
|
{
|
|
int r = color.red();
|
|
int g = color.green();
|
|
int b = color.blue();
|
|
int a = color.alpha();
|
|
|
|
QImage shadow = image;
|
|
for ( int iy = 0; iy < shadow.height(); iy++ )
|
|
{
|
|
auto* scanLine = (QRgb*)shadow.scanLine( iy );
|
|
|
|
for ( int ix = 0; ix < shadow.width(); ix++ )
|
|
{
|
|
scanLine[ix] = qRgba( r, g, b, (a*qAlpha(scanLine[ix]))/255 );
|
|
}
|
|
}
|
|
|
|
return shadow;
|
|
}
|
|
|
|
}
|