diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 500c83d..61618d0 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -44,7 +44,7 @@ jobs: qt-modules: '' name: Linux-Qt5 use-apt: true - apt-packages: 'qtbase5-dev qttools5-dev ninja-build xvfb libxcb-cursor0' + apt-packages: 'qtbase5-dev qttools5-dev libqt5svg5-dev ninja-build xvfb libxcb-cursor0' test-cmd: xvfb-run -a ctest -V -E NOT_BUILT # ========== Windows Builds ========== diff --git a/CHANGELOG.md b/CHANGELOG.md index 0c94410..2966239 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,10 @@ # Changelog +## v2.3.2 + +- support SVG Images +- [#29](https://github.com/procitec/qlitehtmlbrowser/issues/29): Show links to image files in own Dialog + ## v2.3.1 - [#26](https://github.com/procitec/qlitehtmlbrowser/issues/26): Fix Multi-Elemen selection highlight boxes diff --git a/CMakeLists.txt b/CMakeLists.txt index 2d2c9cc..a43ca28 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,6 +1,6 @@ cmake_minimum_required(VERSION 3.28) -project( QLiteHtmlBrowser VERSION 2.3.1 ) +project( QLiteHtmlBrowser VERSION 2.3.2 ) include(GNUInstallDirs) @@ -27,12 +27,12 @@ if(PROJECT_IS_TOP_LEVEL) set( AUTOUIC OFF) set( AUTORCC OFF) - find_package(Qt6 COMPONENTS Core Gui Widgets) + find_package(Qt6 COMPONENTS Core Gui Widgets Svg) if(Qt6_FOUND) set(QT_VERSION_MAJOR 6) else() set(QT_VERSION_MAJOR 5) - find_package(Qt5 5.15 COMPONENTS Core Gui Widgets REQUIRED) + find_package(Qt5 5.15 COMPONENTS Core Gui Widgets Svg REQUIRED) endif() if( WITH_DOCS ) diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 18ec27d..a11879e 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -41,7 +41,7 @@ PRIVATE "${PROJECT_SOURCE_DIR}/include" ) -target_link_libraries(QLiteHtmlBrowser PRIVATE litehtml Qt::Widgets Qt::Gui Qt::Core) +target_link_libraries(QLiteHtmlBrowser PRIVATE litehtml Qt::Widgets Qt::Gui Qt::Svg Qt::Core) set_target_properties(QLiteHtmlBrowser PROPERTIES VERSION ${QLiteHtmlBrowser_VERSION}) set_target_properties(QLiteHtmlBrowser PROPERTIES PUBLIC_HEADER "${PUBLIC_HEADERS}") set_property(TARGET QLiteHtmlBrowser PROPERTY CXX_STANDARD 17) diff --git a/src/QLiteHtmlBrowserImpl.cpp b/src/QLiteHtmlBrowserImpl.cpp index cc048e6..0372b32 100644 --- a/src/QLiteHtmlBrowserImpl.cpp +++ b/src/QLiteHtmlBrowserImpl.cpp @@ -2,7 +2,10 @@ #include "container_qt.h" #include +#include #include +#include +#include #include #include #include @@ -12,6 +15,247 @@ #include #include #include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace +{ + +class ClickableImageLabel final : public QLabel +{ +public: + using ClickHandler = std::function; + + explicit ClickableImageLabel( QWidget* parent = nullptr ) + : QLabel( parent ) + { + } + + void setClickHandler( ClickHandler handler ) { mClickHandler = std::move( handler ); } + +protected: + void mousePressEvent( QMouseEvent* event ) override + { + if ( event && event->button() == Qt::LeftButton ) + { + mLeftButtonPressed = true; + event->accept(); + return; + } + + QLabel::mousePressEvent( event ); + } + + void mouseReleaseEvent( QMouseEvent* event ) override + { +#if QT_VERSION_MAJOR >= 6 + const QPoint releasePos = event ? event->position().toPoint() : QPoint(); +#else + const QPoint releasePos = event ? event->pos() : QPoint(); +#endif + + const bool activate = event && event->button() == Qt::LeftButton && mLeftButtonPressed && rect().contains( releasePos ); + mLeftButtonPressed = false; + + if ( activate ) + { + event->accept(); + if ( mClickHandler ) + { + mClickHandler(); + } + return; + } + + QLabel::mouseReleaseEvent( event ); + } + +private: + ClickHandler mClickHandler = {}; + bool mLeftButtonPressed = false; +}; + +class ImageScrollArea final : public QScrollArea +{ +public: + using LayoutChangedHandler = std::function; + + explicit ImageScrollArea( QWidget* parent = nullptr ) + : QScrollArea( parent ) + { + } + + void setLayoutChangedHandler( LayoutChangedHandler handler ) { mLayoutChangedHandler = std::move( handler ); } + +protected: + void resizeEvent( QResizeEvent* event ) override + { + QScrollArea::resizeEvent( event ); + if ( mLayoutChangedHandler ) + { + mLayoutChangedHandler(); + } + } + + void showEvent( QShowEvent* event ) override + { + QScrollArea::showEvent( event ); + if ( mLayoutChangedHandler ) + { + mLayoutChangedHandler(); + } + } + +private: + LayoutChangedHandler mLayoutChangedHandler = {}; +}; + +QString normalizedUrlPath( const QUrl& url ) +{ + if ( url.isLocalFile() ) + { + return url.toLocalFile(); + } + + return url.path(); +} + +QString urlSuffix( const QUrl& url ) +{ + // return QFileInfo( normalizedUrlPath( url ) ).suffix().toLower(); + return QFileInfo( url.path() ).suffix().toLower(); +} + +static bool hasHtmlExtension( const QUrl& url ) +{ + const auto ext = urlSuffix( url ); + return ext == "html" || ext == "htm" || ext == "xhtml"; +} + +static bool hasImageExtension( const QUrl& url ) +{ + static const QSet exts = { "png", "jpg", "jpeg", "gif", "svg", "bmp", "webp" }; + return exts.contains( urlSuffix( url ) ); +} + +bool isImageData( const QByteArray& data ) +{ + if ( data.isEmpty() ) + { + return false; + } + + QBuffer buffer; + buffer.setData( data ); + if ( !buffer.open( QIODevice::ReadOnly ) ) + { + return false; + } + + QImageReader reader( &buffer ); + reader.setDecideFormatFromContent( true ); + return reader.canRead(); +} + +bool looksLikeSvgData( const QByteArray& data ) +{ + const auto head = QString::fromUtf8( data.left( 512 ) ).trimmed().toLower(); + return head.contains( "( requestedType ); + if ( requested != Browser::ResourceType::Unknown ) + { + return requested; + } + + if ( isImageData( content ) || looksLikeSvgData( content ) ) + { + return Browser::ResourceType::Image; + } + + if ( looksLikeHtmlData( content ) ) + { + return Browser::ResourceType::Html; + } + + if ( hasImageExtension( url ) ) + { + return Browser::ResourceType::Image; + } + + if ( hasHtmlExtension( url ) ) + { + return Browser::ResourceType::Html; + } + + if ( url.isLocalFile() ) + { + QMimeDatabase db; + const auto mime = db.mimeTypeForFile( url.toLocalFile(), QMimeDatabase::MatchContent ); + + if ( mime.inherits( "text/html" ) || mime.inherits( "application/xhtml+xml" ) ) + { + return Browser::ResourceType::Html; + } + + if ( mime.name().startsWith( "image/" ) ) + { + return Browser::ResourceType::Image; + } + } + + return Browser::ResourceType::Unknown; +} + +bool isSvgUrl( const QUrl& url ) +{ + return urlSuffix( url ) == "svg"; +} + +QSize toLogicalPixels( const QSize& pixelSize, qreal dpr ) +{ + const qreal safeDpr = dpr > 0.0 ? dpr : 1.0; + return QSize( std::max( 1, static_cast( std::ceil( pixelSize.width() / safeDpr ) ) ), + std::max( 1, static_cast( std::ceil( pixelSize.height() / safeDpr ) ) ) ); +} + +QSize toDevicePixels( const QSize& logicalSize, qreal dpr ) +{ + const qreal safeDpr = dpr > 0.0 ? dpr : 1.0; + return QSize( std::max( 1, static_cast( std::round( logicalSize.width() * safeDpr ) ) ), + std::max( 1, static_cast( std::round( logicalSize.height() * safeDpr ) ) ) ); +} + +} // namespace QLiteHtmlBrowserImpl::QLiteHtmlBrowserImpl( QWidget* parent ) : QWidget( parent ) @@ -27,10 +271,36 @@ QLiteHtmlBrowserImpl::QLiteHtmlBrowserImpl( QWidget* parent ) connect( mContainer, &container_qt::scaleChanged, this, &QLiteHtmlBrowserImpl::scaleChanged ); connect( mContainer, &container_qt::selectionChanged, this, &QLiteHtmlBrowserImpl::selectionChanged ); + auto* imageScroll = new ImageScrollArea( this ); + imageScroll->setWidgetResizable( false ); + imageScroll->setAlignment( Qt::AlignCenter ); + imageScroll->setLayoutChangedHandler( + [this]() + { + if ( mViewStack && mImageScroll && mViewStack->currentWidget() == mImageScroll && mImageFitToView ) + { + updateImageView(); + } + } ); + + auto* imageLabel = new ClickableImageLabel( imageScroll ); + imageLabel->setAlignment( Qt::AlignCenter ); + imageLabel->setSizePolicy( QSizePolicy::Fixed, QSizePolicy::Fixed ); + imageLabel->setClickHandler( [this]() { toggleImageZoomMode(); } ); + + imageScroll->setWidget( imageLabel ); + mImageScroll = imageScroll; + mImageLabel = imageLabel; + auto* layout = new QVBoxLayout; layout->setContentsMargins( 0, 0, 0, 0 ); - layout->addWidget( mContainer ); + mViewStack = new QStackedLayout; + mViewStack->setContentsMargins( 0, 0, 0, 0 ); + mViewStack->addWidget( mContainer ); + mViewStack->addWidget( mImageScroll ); + layout->addLayout( mViewStack ); setLayout( layout ); + showHtmlView(); applyCSS(); } @@ -158,71 +428,407 @@ void QLiteHtmlBrowserImpl::mousePressEvent( QMouseEvent* e ) e->ignore(); } -// void QLiteHtmlBrowser::resizeEvent( QResizeEvent* ev ) -//{ -// if ( ev ) -// { -// QWidget::resizeEvent( ev ); -// } -//} +bool QLiteHtmlBrowserImpl::isImageUrl( const QUrl& u ) const +{ + return hasImageExtension( u ); +} + +bool QLiteHtmlBrowserImpl::isHtmlUrl( const QUrl& u ) const +{ + return hasHtmlExtension( u ); +} void QLiteHtmlBrowserImpl::setUrl( const QUrl& url, int type, bool clearFWHist ) { - mUrl = UrlType( url, type ); + if ( !mContainer ) + { + return; + } + + QUrl pureUrl( url ); + pureUrl.setFragment( {} ); + + QByteArray content; + + if ( pureUrl.isLocalFile() ) + { + QFile f( pureUrl.toLocalFile() ); + if ( f.open( QIODevice::ReadOnly ) ) + { + content = f.readAll(); + f.close(); + } + } + else + { + // NICHT über findFile/loadResource laufen lassen. + // qthelp:, http:, custom schemes etc. müssen hier bleiben. + content = mResourceHandler( type, url ); + + // optional: falls type Unknown ist und dein Handler den Typ braucht: + if ( content.isEmpty() && type == static_cast( Browser::ResourceType::Unknown ) ) + { + if ( hasHtmlExtension( pureUrl ) ) + { + content = mResourceHandler( static_cast( Browser::ResourceType::Html ), url ); + type = static_cast( Browser::ResourceType::Html ); + } + else if ( hasImageExtension( pureUrl ) ) + { + content = mResourceHandler( static_cast( Browser::ResourceType::Image ), url ); + type = static_cast( Browser::ResourceType::Image ); + } + } + } + + if ( content.isEmpty() ) + { + return; + } + + const bool isHtml = hasHtmlExtension( pureUrl ) || looksLikeHtmlData( content ); + const bool isImage = hasImageExtension( pureUrl ) || isImageData( content ) || looksLikeSvgData( content ); + + if ( isHtml ) + { + parseUrl( url ); + mContainer->setHtml( QString::fromUtf8( content ), url ); + mCurrentCaption = mContainer->caption(); + showHtmlView(); + } + else if ( isImage ) + { + if ( !showImageFromData( url, content ) ) + { + return; + } + } + else + { + return; + } + + const int storedType = isHtml ? static_cast( Browser::ResourceType::Html ) : isImage ? static_cast( Browser::ResourceType::Image ) : type; + + mUrl = UrlType( url, storedType ); + auto [home_url, home_type] = mHome; if ( home_url.isEmpty() ) { - mHome = UrlType( url, type ); + mHome = UrlType( url, storedType ); } - if ( mContainer ) + QUrl hist_url; + if ( !mBWHistStack.isEmpty() ) + { + hist_url = mBWHistStack.top().url; + } + + if ( hist_url != url ) { - auto pure_url = QUrl( url ); - pure_url.setFragment( {} ); - QString html; + mBWHistStack.push( { url, storedType, caption() } ); + } - if ( pure_url.isLocalFile() ) + if ( clearFWHist ) + { + mFWHistStack.clear(); + } + + update(); + emit urlChanged( url ); +} + +QSize QLiteHtmlBrowserImpl::imageViewportSize() const +{ + if ( !mImageScroll ) + { + return {}; + } + + QSize viewportSize = mImageScroll->viewport()->size(); + viewportSize -= QSize( 4, 4 ); + return viewportSize.expandedTo( QSize( 1, 1 ) ); +} + +qreal QLiteHtmlBrowserImpl::imageDevicePixelRatio() const +{ + if ( mImageScroll && mImageScroll->viewport() ) + { +#if QT_VERSION >= QT_VERSION_CHECK( 5, 6, 0 ) + const qreal viewportDpr = mImageScroll->viewport()->devicePixelRatioF(); +#else + const qreal viewportDpr = mImageScroll->viewport()->devicePixelRatio(); +#endif + if ( viewportDpr > 0.0 ) { - // get the content of the url and display the html + return viewportDpr; + } + } - QFile f( pure_url.toLocalFile() ); - if ( f.open( QIODevice::ReadOnly ) ) + if ( const auto* topLevel = window() ) + { + if ( auto* handle = topLevel->windowHandle() ) + { + if ( auto* currentScreen = handle->screen() ) { - html = f.readAll(); - f.close(); + const qreal screenDpr = currentScreen->devicePixelRatio(); + if ( screenDpr > 0.0 ) + { + return screenDpr; + } } } - else + } + + if ( auto* currentScreen = QApplication::primaryScreen() ) + { + const qreal screenDpr = currentScreen->devicePixelRatio(); + if ( screenDpr > 0.0 ) + { + return screenDpr; + } + } + + return 1.0; +} + +QSize QLiteHtmlBrowserImpl::imageNaturalDisplaySize() const +{ + if ( mCurrentImageIsSvg ) + { + if ( mCurrentSvgDefaultSize.isValid() && !mCurrentSvgDefaultSize.isEmpty() ) + { + return mCurrentSvgDefaultSize; + } + return QSize( 512, 512 ); + } + + if ( !mCurrentImage.isNull() ) + { + return toLogicalPixels( mCurrentImage.size(), imageDevicePixelRatio() ); + } + + return {}; +} + +bool QLiteHtmlBrowserImpl::imageFitsViewport( const QSize& imageSize ) const +{ + const QSize viewportSize = imageViewportSize(); + return imageSize.width() <= viewportSize.width() && imageSize.height() <= viewportSize.height(); +} + +QPixmap QLiteHtmlBrowserImpl::createRasterDisplayPixmap( const QSize& logicalSize, qreal devicePixelRatio ) const +{ + if ( mCurrentImage.isNull() || !logicalSize.isValid() || logicalSize.isEmpty() ) + { + return {}; + } + + const QSize targetPixelSize = toDevicePixels( logicalSize, devicePixelRatio ); + QImage displayImage = mCurrentImage; + + if ( displayImage.size() != targetPixelSize ) + { + displayImage = mCurrentImage.scaled( targetPixelSize, Qt::KeepAspectRatio, Qt::SmoothTransformation ); + } + + QPixmap displayPixmap = QPixmap::fromImage( displayImage ); + displayPixmap.setDevicePixelRatio( devicePixelRatio > 0.0 ? devicePixelRatio : 1.0 ); + return displayPixmap; +} + +QPixmap QLiteHtmlBrowserImpl::createSvgDisplayPixmap( const QSize& logicalSize, qreal devicePixelRatio ) const +{ + if ( !mCurrentImageIsSvg || mCurrentSvgData.isEmpty() || !logicalSize.isValid() || logicalSize.isEmpty() ) + { + return {}; + } + + QSvgRenderer renderer( mCurrentSvgData ); + if ( !renderer.isValid() ) + { + return {}; + } + + const QSize targetPixelSize = toDevicePixels( logicalSize, devicePixelRatio ); + QImage displayImage( targetPixelSize, QImage::Format_ARGB32_Premultiplied ); + displayImage.fill( Qt::transparent ); + + QPainter painter( &displayImage ); + renderer.render( &painter, QRect( QPoint( 0, 0 ), targetPixelSize ) ); + painter.end(); + + QPixmap displayPixmap = QPixmap::fromImage( displayImage ); + displayPixmap.setDevicePixelRatio( devicePixelRatio > 0.0 ? devicePixelRatio : 1.0 ); + return displayPixmap; +} + +QPixmap QLiteHtmlBrowserImpl::createDisplayPixmap( const QSize& logicalSize, qreal devicePixelRatio ) const +{ + if ( mCurrentImageIsSvg ) + { + return createSvgDisplayPixmap( logicalSize, devicePixelRatio ); + } + + return createRasterDisplayPixmap( logicalSize, devicePixelRatio ); +} + +void QLiteHtmlBrowserImpl::updateImageView() +{ + if ( !mImageLabel || !mImageScroll ) + { + return; + } + + if ( !mCurrentImageIsSvg && mCurrentImage.isNull() ) + { + return; + } + + QSize displayLogicalSize = imageNaturalDisplaySize(); + if ( !displayLogicalSize.isValid() || displayLogicalSize.isEmpty() ) + { + return; + } + + if ( mImageFitToView ) + { + const QSize viewportSize = imageViewportSize(); + if ( viewportSize.isValid() && !imageFitsViewport( displayLogicalSize ) ) + { + displayLogicalSize = displayLogicalSize.scaled( viewportSize, Qt::KeepAspectRatio ); + } + } + + const qreal devicePixelRatio = imageDevicePixelRatio(); + const QPixmap displayPixmap = createDisplayPixmap( displayLogicalSize, devicePixelRatio ); + if ( displayPixmap.isNull() ) + { + return; + } + + mImageLabel->setPixmap( displayPixmap ); + mImageLabel->resize( displayLogicalSize ); + mImageLabel->setMinimumSize( displayLogicalSize ); + mImageLabel->setMaximumSize( displayLogicalSize ); + mImageScroll->horizontalScrollBar()->setValue( 0 ); + mImageScroll->verticalScrollBar()->setValue( 0 ); +} + +void QLiteHtmlBrowserImpl::toggleImageZoomMode() +{ + const QSize naturalDisplaySize = imageNaturalDisplaySize(); + if ( naturalDisplaySize.isEmpty() || imageFitsViewport( naturalDisplaySize ) ) + { + return; + } + + mImageFitToView = !mImageFitToView; + updateImageView(); +} + +bool QLiteHtmlBrowserImpl::showImageFromData( const QUrl& url, const QByteArray& imageData ) +{ + if ( !mImageLabel || !mImageScroll ) + { + return false; + } + + mCurrentImage = QImage(); + mCurrentSvgData.clear(); + mCurrentSvgDefaultSize = {}; + mCurrentImageIsSvg = false; + + auto tryLoadSvg = [this]( const QByteArray& data ) -> bool + { + if ( data.isEmpty() ) { - // eg. if ( url.scheme() == "qthelp" ) - html = mResourceHandler( type, url ); + return false; } - if ( !html.isEmpty() ) + QSvgRenderer renderer( data ); + if ( !renderer.isValid() ) { - parseUrl( url ); - mContainer->setHtml( html, url ); + return false; + } - auto hist_url = QUrl(); + mCurrentSvgData = data; + mCurrentSvgDefaultSize = renderer.defaultSize(); + if ( !mCurrentSvgDefaultSize.isValid() || mCurrentSvgDefaultSize.isEmpty() ) + { + mCurrentSvgDefaultSize = QSize( 512, 512 ); + } + mCurrentImageIsSvg = true; + return true; + }; - if ( !mBWHistStack.isEmpty() ) + bool loaded = false; + + if ( !imageData.isEmpty() ) + { + if ( ( isSvgUrl( url ) || looksLikeSvgData( imageData ) ) && tryLoadSvg( imageData ) ) + { + loaded = true; + } + else + { + QImage img; + img.loadFromData( imageData ); + if ( !img.isNull() ) { - hist_url = mBWHistStack.top().url; + mCurrentImage = img; + mCurrentImageIsSvg = false; + loaded = true; } + } + } - if ( hist_url != url ) + if ( !loaded && url.isLocalFile() ) + { + QFileInfo f( url.toLocalFile() ); + if ( f.exists() ) + { + if ( isSvgUrl( url ) ) { - mBWHistStack.push( { url, type, mContainer->caption() } ); + QFile svgFile( f.absoluteFilePath() ); + if ( svgFile.open( QIODevice::ReadOnly ) ) + { + const QByteArray svgData = svgFile.readAll(); + svgFile.close(); + loaded = tryLoadSvg( svgData ); + } } - if ( clearFWHist ) - mFWHistStack.clear(); - - update(); + if ( !loaded ) + { + QImage img; + if ( img.load( f.absoluteFilePath() ) ) + { + mCurrentImage = img; + mCurrentImageIsSvg = false; + loaded = true; + } + } } + } - emit urlChanged( url ); + if ( !loaded ) + { + return false; } + + mImageFitToView = true; + + QFileInfo info( normalizedUrlPath( url ) ); + mCurrentCaption = info.fileName().isEmpty() ? url.fileName() : info.fileName(); + if ( mCurrentCaption.isEmpty() ) + { + mCurrentCaption = url.toDisplayString(); + } + + showImageView(); + updateImageView(); + return true; } QUrl QLiteHtmlBrowserImpl::baseUrl( const QUrl& url ) const @@ -243,6 +849,8 @@ void QLiteHtmlBrowserImpl::setHtml( const QString& html, const QUrl& source_url { parseUrl( source_url ); mContainer->setHtml( html, source_url ); + mCurrentCaption = mContainer->caption(); + showHtmlView(); } } @@ -290,18 +898,74 @@ double QLiteHtmlBrowserImpl::scale() const return scale; } -QByteArray QLiteHtmlBrowserImpl::loadResource( int /*type*/, const QUrl& url ) +QImage QLiteHtmlBrowserImpl::loadSvgFromFile( const QString& filename ) +{ + QSvgRenderer renderer; + QImage img; + renderer.load( filename ); + if ( renderer.isValid() ) + { + QSize size = renderer.defaultSize(); + if ( !size.isValid() || size.isEmpty() ) + { + size = QSize( 512, 512 ); + } + QImage svgImg( size, QImage::Format_ARGB32_Premultiplied ); + svgImg.fill( Qt::transparent ); + QPainter p( &svgImg ); + renderer.render( &p ); + img = svgImg; + } + return img; +} + +QImage QLiteHtmlBrowserImpl::loadSvgFromData( const QByteArray& data ) +{ + QSvgRenderer renderer; + QImage img; + renderer.load( data ); + if ( renderer.isValid() ) + { + QSize size = renderer.defaultSize(); + if ( !size.isValid() || size.isEmpty() ) + { + size = QSize( 512, 512 ); + } + QImage svgImg( size, QImage::Format_ARGB32_Premultiplied ); + svgImg.fill( Qt::transparent ); + QPainter p( &svgImg ); + renderer.render( &p ); + img = svgImg; + } + return img; +} + +QByteArray QLiteHtmlBrowserImpl::loadResource( int type, const QUrl& url ) { QByteArray data; + auto resource_type = static_cast( type ); + QString fileName = findFile( url ); if ( !fileName.isEmpty() ) { - QFile f( fileName ); - if ( f.open( QFile::ReadOnly ) ) + if ( resource_type == Browser::ResourceType::Image && fileName.toLower().endsWith( ".svg" ) ) { - data = f.readAll(); - f.close(); + auto img = loadSvgFromFile( fileName ); + auto pixmap = QPixmap::fromImage( img ); + QBuffer buffer( &data ); + buffer.open( QIODevice::WriteOnly ); + pixmap.save( &buffer, "PNG" ); + buffer.close(); + } + else + { + QFile f( fileName ); + if ( f.open( QFile::ReadOnly ) ) + { + data = f.readAll(); + f.close(); + } } } @@ -330,12 +994,13 @@ QUrl QLiteHtmlBrowserImpl::resolveUrl( const QString& url ) { resolved = QUrl( mBaseUrl ).resolved( _url ); } + if ( !resolved.isRelative() ) { return resolved; } - else if ( QFileInfo( resolved.toLocalFile() ).isReadable() ) + if ( QFileInfo( resolved.toLocalFile() ).isReadable() ) { return QUrl::fromLocalFile( resolved.toLocalFile() ); } @@ -447,6 +1112,7 @@ void QLiteHtmlBrowserImpl::backward() setUrl( entry.url, entry.urlType, false /* don’t clear forward history */ ); } } + void QLiteHtmlBrowserImpl::reload() { if ( !mBWHistStack.isEmpty() ) @@ -458,7 +1124,7 @@ void QLiteHtmlBrowserImpl::reload() const QString& QLiteHtmlBrowserImpl::caption() const { - return mContainer->caption(); + return mCurrentCaption; } void QLiteHtmlBrowserImpl::print( QPagedPaintDevice* printer ) const @@ -495,6 +1161,7 @@ void QLiteHtmlBrowserImpl::previousFindMatch() mContainer->findPreviousMatch(); } } + QString QLiteHtmlBrowserImpl::selectedText() const { QString text; @@ -504,3 +1171,42 @@ QString QLiteHtmlBrowserImpl::selectedText() const } return text; } + +void QLiteHtmlBrowserImpl::showHtmlView() +{ + if ( mViewStack && mContainer ) + { + mViewStack->setCurrentWidget( mContainer ); + } +} + +void QLiteHtmlBrowserImpl::showImageView() +{ + if ( mViewStack && mImageScroll ) + { + const bool wasImageView = mViewStack->currentWidget() == mImageScroll; + mViewStack->setCurrentWidget( mImageScroll ); + + if ( wasImageView ) + { + updateImageView(); + } + } +} + +bool QLiteHtmlBrowserImpl::onImageClicked( const QUrl& url ) +{ + QByteArray imageData = loadResource( static_cast( Browser::ResourceType::Image ), url ); + + if ( imageData.isEmpty() && url.isLocalFile() ) + { + QFile f( url.toLocalFile() ); + if ( f.open( QIODevice::ReadOnly ) ) + { + imageData = f.readAll(); + f.close(); + } + } + + return showImageFromData( url, imageData ); +} diff --git a/src/QLiteHtmlBrowserImpl.h b/src/QLiteHtmlBrowserImpl.h index 95b6253..f696556 100644 --- a/src/QLiteHtmlBrowserImpl.h +++ b/src/QLiteHtmlBrowserImpl.h @@ -4,6 +4,9 @@ class container_qt; #include "browserdefinitions.h" #include +#include +#include +#include #include #include #include @@ -106,23 +109,50 @@ class QLiteHtmlBrowserImpl : public QWidget }; QString findFile( const QUrl& name ) const; + void showHtmlView(); + void showImageView(); void onAnchorClicked( const QUrl& ); QUrl baseUrl( const QUrl& url ) const; void parseUrl( const QUrl& url ); QString readResourceCss( const QString& ) const; void applyCSS(); + bool isImageUrl( const QUrl& u ) const; + bool isHtmlUrl( const QUrl& u ) const; + bool onImageClicked( const QUrl& url ); + void updateImageView(); + void toggleImageZoomMode(); + // bool imageFitsViewport( const QSize& imageSize ) const; + QSize imageViewportSize() const; + qreal imageDevicePixelRatio() const; + QSize imageNaturalDisplaySize() const; + bool imageFitsViewport( const QSize& imageSize ) const; + QPixmap createRasterDisplayPixmap( const QSize& logicalSize, qreal devicePixelRatio ) const; + QPixmap createSvgDisplayPixmap( const QSize& logicalSize, qreal devicePixelRatio ) const; + QPixmap createDisplayPixmap( const QSize& logicalSize, qreal devicePixelRatio ) const; + QImage loadSvgFromFile( const QString& filename ); + QImage loadSvgFromData( const QByteArray& data ); + bool showImageFromData( const QUrl& url, const QByteArray& imageData ); Q_DISABLE_COPY_MOVE( QLiteHtmlBrowserImpl ); - container_qt* mContainer = nullptr; - QUrl mBaseUrl = {}; - QString mExternalCSS = {}; - UrlType mUrl = {}; + container_qt* mContainer = nullptr; + QStackedLayout* mViewStack = nullptr; + QScrollArea* mImageScroll = nullptr; + QLabel* mImageLabel = nullptr; + QImage mCurrentImage = {}; + QByteArray mCurrentSvgData = {}; + QSize mCurrentSvgDefaultSize = {}; + bool mCurrentImageIsSvg = false; + bool mImageFitToView = true; + QUrl mBaseUrl = {}; + QString mExternalCSS = {}; + UrlType mUrl = {}; Browser::ResourceHandlerType mResourceHandler; Browser::UrlResolveHandlerType mUrlResolveHandler; - QStack mBWHistStack = {}; - QStack mFWHistStack = {}; - UrlType mHome = {}; - QStringList mSearchPaths = {}; - QStringList mValidSchemes = { "file", "qrc", "qthelp" }; + QStack mBWHistStack = {}; + QStack mFWHistStack = {}; + UrlType mHome = {}; + QString mCurrentCaption = {}; + QStringList mSearchPaths = {}; + QStringList mValidSchemes = { "file", "qrc", "qthelp" }; }; diff --git a/test/browser/CMakeLists.txt b/test/browser/CMakeLists.txt index b541018..b12795b 100644 --- a/test/browser/CMakeLists.txt +++ b/test/browser/CMakeLists.txt @@ -1,11 +1,11 @@ project( TestBrowser ) -find_package(Qt6 COMPONENTS Core Gui Widgets Help) +find_package(Qt6 COMPONENTS Core Gui Widgets Svg Help) if(Qt6_FOUND) set(QT_VERSION_MAJOR 6) else() set(QT_VERSION_MAJOR 5) - find_package(Qt5 5.15 COMPONENTS Core Gui Widgets Help REQUIRED) + find_package(Qt5 5.15 COMPONENTS Core Gui Widgets Svg Help REQUIRED) endif() add_executable( testbrowser ) @@ -26,4 +26,4 @@ target_sources( testbrowser ) target_include_directories(testbrowser PRIVATE "${QLiteHtmlBrowser_SOURCE_DIR}/include") -target_link_libraries( testbrowser PRIVATE QLiteHtmlBrowser Qt::Widgets Qt::Gui Qt::Core Qt::Help ) +target_link_libraries( testbrowser PRIVATE QLiteHtmlBrowser Qt::Widgets Qt::Gui Qt::Core Qt::Svg Qt::Help ) diff --git a/test/browser/files/images_01/images/plantuml.svg b/test/browser/files/images_01/images/plantuml.svg new file mode 100644 index 0000000..899a182 --- /dev/null +++ b/test/browser/files/images_01/images/plantuml.svg @@ -0,0 +1 @@ +yesa?activitydo ado bnob?yesnoc?yesyese?do somethingnod?yesdummysome functionnoc?yesyesf?noactivity \ No newline at end of file diff --git a/test/browser/files/images_01/index-linkedimages.html b/test/browser/files/images_01/index-linkedimages.html new file mode 100644 index 0000000..24b10a2 --- /dev/null +++ b/test/browser/files/images_01/index-linkedimages.html @@ -0,0 +1,31 @@ + + + +Images Demo + + + +
+ unscaled gradient jpg +

Unscaled image (jpg) from subdirectory.

+
+ +
+ unscaled procitec png +

Scaled image (png) from subdirectory.

+
+ +
+ unscaled plantuml svg +

Scaled image (svg) from subdirectory.

+
+ +
+ broken url jpg +

Invalid URL.

+
+ + + + + diff --git a/test/browser/testbrowser.cpp b/test/browser/testbrowser.cpp index 9d3af09..c9c3063 100644 --- a/test/browser/testbrowser.cpp +++ b/test/browser/testbrowser.cpp @@ -5,18 +5,121 @@ #include #include #include +#include +#include // #include #include #include #include +#include #include #include #include +namespace +{ + +constexpr const char* SVG_COLOR_TOKEN = "#000000"; + +const QByteArray HOME_SVG = QByteArrayLiteral( + R"svg()svg" ); + +const QByteArray BACK_SVG = QByteArrayLiteral( + R"svg()svg" ); + +const QByteArray FORWARD_SVG = QByteArrayLiteral( + R"svg()svg" ); + +const QByteArray UP_SVG = QByteArrayLiteral( + R"svg()svg" ); + +const QByteArray DOWN_SVG = QByteArrayLiteral( + R"svg()svg" ); + +QByteArray recolorSvg( QByteArray svg, const QColor& color ) +{ + svg.replace( SVG_COLOR_TOKEN, color.name( QColor::HexRgb ).toUtf8() ); + return svg; +} + +QPixmap renderSvgPixmap( const QByteArray& svgTemplate, const QColor& color, const QSize& logicalSize, qreal devicePixelRatio ) +{ + const QSize deviceSize( qMax( 1, qRound( logicalSize.width() * devicePixelRatio ) ), qMax( 1, qRound( logicalSize.height() * devicePixelRatio ) ) ); + + QPixmap pixmap( deviceSize ); + pixmap.fill( Qt::transparent ); + pixmap.setDevicePixelRatio( devicePixelRatio ); + + QSvgRenderer renderer; + renderer.load( recolorSvg( svgTemplate, color ) ); + if ( !renderer.isValid() ) + { + return pixmap; + } + + QPainter painter( &pixmap ); + painter.setRenderHint( QPainter::Antialiasing, true ); + painter.setRenderHint( QPainter::SmoothPixmapTransform, true ); + renderer.render( &painter, QRectF( QPointF( 0.0, 0.0 ), QSizeF( logicalSize ) ) ); + + return pixmap; +} + +QIcon createSvgIcon( const QByteArray& svgTemplate, const QWidget* widget ) +{ + const QPalette palette = widget->palette(); + + QColor normalColor = palette.color( QPalette::ButtonText ); + if ( !normalColor.isValid() ) + { + normalColor = palette.color( QPalette::WindowText ); + } + + QColor disabledColor = palette.color( QPalette::Disabled, QPalette::ButtonText ); + if ( !disabledColor.isValid() ) + { + disabledColor = palette.color( QPalette::Disabled, QPalette::WindowText ); + } + if ( !disabledColor.isValid() ) + { + disabledColor = normalColor; + } + + QIcon icon; + const QList iconExtents = { 16, 20, 24, 32 }; + const QList devicePixelRatios = { 1.0, 1.25, 1.5, 2.0 }; + + for ( const int iconExtent : iconExtents ) + { + const QSize logicalSize( iconExtent, iconExtent ); + for ( const qreal devicePixelRatio : devicePixelRatios ) + { + icon.addPixmap( renderSvgPixmap( svgTemplate, normalColor, logicalSize, devicePixelRatio ), QIcon::Normal, QIcon::Off ); + icon.addPixmap( renderSvgPixmap( svgTemplate, normalColor, logicalSize, devicePixelRatio ), QIcon::Active, QIcon::Off ); + icon.addPixmap( renderSvgPixmap( svgTemplate, disabledColor, logicalSize, devicePixelRatio ), QIcon::Disabled, QIcon::Off ); + } + } + + return icon; +} + +int toolbarIconExtent( const QWidget* widget ) +{ + return widget->style()->pixelMetric( QStyle::PM_ToolBarIconSize, nullptr, widget ); +} + +} // namespace + TestBrowser::TestBrowser() { + const auto homeIcon = createSvgIcon( HOME_SVG, this ); + const auto backIcon = createSvgIcon( BACK_SVG, this ); + const auto forwardIcon = createSvgIcon( FORWARD_SVG, this ); + const auto upIcon = createSvgIcon( UP_SVG, this ); + const auto downIcon = createSvgIcon( DOWN_SVG, this ); + mBrowser = new QHelpBrowser( this ); setCentralWidget( mBrowser ); setSizePolicy( QSizePolicy::MinimumExpanding, QSizePolicy::MinimumExpanding ); @@ -24,6 +127,9 @@ TestBrowser::TestBrowser() resize( 1024, 768 ); mBrowser->show(); + const int iconExtent = toolbarIconExtent( this ); + mToolBar.setIconSize( QSize( iconExtent, iconExtent ) ); + auto file_menu = mMenu.addMenu( "File" ); auto action = new QAction( this ); @@ -72,6 +178,20 @@ TestBrowser::TestBrowser() } } ); + mActHome = new QAction( homeIcon, tr( "home" ), this ); + connect( mActHome, &QAction::triggered, this, [this]() { home(); } ); + mToolBar.addAction( mActHome ); + + mActBackward = new QAction( backIcon, tr( "backward" ), this ); + connect( mActBackward, &QAction::triggered, this, [this]() { backward(); } ); + mToolBar.addAction( mActBackward ); + + mActForward = new QAction( forwardIcon, tr( "forward" ), this ); + connect( mActForward, &QAction::triggered, this, [this]() { forward(); } ); + mToolBar.addAction( mActForward ); + + mToolBar.addSeparator(); + mFindText = new QLineEdit( this ); mToolBar.addWidget( mFindText ); connect( mFindText, &QLineEdit::returnPressed, this, @@ -83,13 +203,12 @@ TestBrowser::TestBrowser() } } ); - // Vorheriges mit Standard-Icon (Pfeil nach oben) - mPreviousFindMatch = new QAction( style()->standardIcon( QStyle::SP_ArrowUp ), "Vorheriges", this ); + mPreviousFindMatch = new QAction( upIcon, "Vorheriges", this ); connect( mPreviousFindMatch, &QAction::triggered, this, [this]() { previousFindMatch(); } ); mPreviousFindMatch->setEnabled( false ); mToolBar.addAction( mPreviousFindMatch ); - mNextFindMatch = new QAction( style()->standardIcon( QStyle::SP_ArrowDown ), "Nächstes", this ); + mNextFindMatch = new QAction( downIcon, "Nächstes", this ); mNextFindMatch->setEnabled( false ); connect( mNextFindMatch, &QAction::triggered, this, [this]() { nextFindMatch(); } ); @@ -244,3 +363,18 @@ void TestBrowser::nextFindMatch() { mBrowser->findNextMatch(); } + +void TestBrowser::forward() +{ + mBrowser->forward(); +} + +void TestBrowser::backward() +{ + mBrowser->backward(); +} + +void TestBrowser::home() +{ + mBrowser->home(); +} diff --git a/test/browser/testbrowser.h b/test/browser/testbrowser.h index c1ae0cd..2798776 100644 --- a/test/browser/testbrowser.h +++ b/test/browser/testbrowser.h @@ -41,6 +41,9 @@ class TestBrowser : public QMainWindow void nextFindMatch(); void previousFindMatch(); bool loadTestFonts(); + void home(); + void forward(); + void backward(); private: QHelpBrowser* mBrowser; @@ -56,4 +59,7 @@ class TestBrowser : public QMainWindow QString mScaleText = tr( "Zoom: %1%" ); QLabel* mSelection = nullptr; QString mSelectionText = tr( "Selected %1 chars" ); + QAction* mActHome = nullptr; + QAction* mActForward = nullptr; + QAction* mActBackward = nullptr; }; diff --git a/test/library/CMakeLists.txt b/test/library/CMakeLists.txt index 12d40d4..c2d6bd3 100644 --- a/test/library/CMakeLists.txt +++ b/test/library/CMakeLists.txt @@ -1,10 +1,10 @@ project( QLiteHtmlBrowserTest ) -find_package(Qt6 COMPONENTS Core Gui Widgets Test) +find_package(Qt6 COMPONENTS Core Gui Widgets Svg Test) if(Qt6_FOUND) set(QT_VERSION_MAJOR 6) else() - find_package(Qt5 5.15 COMPONENTS Core Gui Widgets Test REQUIRED) + find_package(Qt5 5.15 COMPONENTS Core Gui Widgets Svg Test REQUIRED) set(QT_VERSION_MAJOR 5) endif() @@ -60,7 +60,7 @@ foreach( name ${test_names}) target_include_directories( ${name} PRIVATE ${QLiteHtmlBrowser_SOURCE_DIR}/include ${QLiteHtmlBrowser_SOURCE_DIR}/src) - target_link_libraries(${name} PUBLIC litehtml Qt::Widgets Qt::Gui Qt::Test ) + target_link_libraries(${name} PUBLIC litehtml Qt::Widgets Qt::Gui Qt::Svg Qt::Test ) target_compile_definitions(${name} PRIVATE TEST_SOURCE_DIR="${CMAKE_CURRENT_SOURCE_DIR}") @@ -69,6 +69,18 @@ foreach( name ${test_names}) set_tests_properties(${name} PROPERTIES ENVIRONMENT "${environment}" ) +endforeach() +qt_wrap_cpp ( test_history_image_mocs + test_history_image.h + ${QLiteHtmlBrowser_SOURCE_DIR}/include/qlitehtmlbrowser/QLiteHtmlBrowser.h +) -endforeach() +add_executable( test_history_image test_history_image.cpp test_history_image.h ${test_history_image_mocs}) +target_include_directories( test_history_image PRIVATE ${QLiteHtmlBrowser_SOURCE_DIR}/include ${QLiteHtmlBrowser_SOURCE_DIR}/src) +target_link_libraries(test_history_image PUBLIC QLiteHtmlBrowser litehtml Qt::Widgets Qt::Gui Qt::Svg Qt::Test ) + +add_test(NAME test_history_image COMMAND test_history_image) +set_tests_properties(test_history_image PROPERTIES + ENVIRONMENT "${environment}" +) diff --git a/test/library/images/plantuml.jpg b/test/library/images/plantuml.jpg new file mode 100644 index 0000000..2f293a2 Binary files /dev/null and b/test/library/images/plantuml.jpg differ diff --git a/test/library/images/plantuml.svg b/test/library/images/plantuml.svg new file mode 100644 index 0000000..899a182 --- /dev/null +++ b/test/library/images/plantuml.svg @@ -0,0 +1 @@ +yesa?activitydo ado bnob?yesnoc?yesyese?do somethingnod?yesdummysome functionnoc?yesyesf?noactivity \ No newline at end of file diff --git a/test/library/test_history_image.cpp b/test/library/test_history_image.cpp new file mode 100644 index 0000000..4c91e14 --- /dev/null +++ b/test/library/test_history_image.cpp @@ -0,0 +1,109 @@ +#include "test_history_image.h" + +#include + +QTEST_MAIN( TestQLiteHtmlBrowserHistory ) + +QString TestQLiteHtmlBrowserHistory::writeFile( const QString& name, const QByteArray& content ) +{ + const QString path = mTempDir.filePath( name ); + QFile f( path ); + ( f.open( QIODevice::WriteOnly | QIODevice::Truncate ), qPrintable( path ) ); + ( f.write( content ), content.size() ); + f.close(); + return path; +} + +QString TestQLiteHtmlBrowserHistory::createTestImage( const QString& name, const QSize& size ) +{ + const QString path = mTempDir.filePath( name ); + + QImage img( size, QImage::Format_ARGB32_Premultiplied ); + img.fill( Qt::red ); + ( img.save( path ), qPrintable( path ) ); + + return path; +} + +QUrl TestQLiteHtmlBrowserHistory::createHtmlPageWithImageLink( const QString& htmlName, const QString& imageFileName ) +{ + const QByteArray html = QByteArray( R"( + + + + Test HTML Page + + +

Before image

+ Open image + + +)" ); + return QUrl::fromLocalFile( writeFile( htmlName, html ) ); +} + +void TestQLiteHtmlBrowserHistory::imageUrlIsAddedToHistory() +{ + const QString imagePath = createTestImage( "image.png" ); + const QUrl htmlUrl = createHtmlPageWithImageLink( "page1.html", "image.png" ); + const QUrl imageUrl = QUrl::fromLocalFile( imagePath ); + + QLiteHtmlBrowser browser; + browser.resize( 800, 600 ); + browser.show(); + QVERIFY( QTest::qWaitForWindowExposed( &browser ) ); + + // Falls vorhanden: Signal auf URL-Wechsel beobachten + QSignalSpy urlChangedSpy( &browser, SIGNAL(urlChanged(QUrl))); + + // 1) HTML laden + browser.setSource( htmlUrl ); + QTRY_COMPARE( browser.caption(), QString( "Test HTML Page" ) ); + + // 2) Bild laden (entspricht funktional dem Klick auf den Datei-Link) + browser.setSource( imageUrl ); + + // Erwartung: Bildansicht aktiv, Caption ist Dateiname + QTRY_COMPARE( browser.caption(), QString( "image.png" ) ); + + // 3) Zurück => wieder HTML + browser.backward(); + QTRY_COMPARE( browser.caption(), QString( "Test HTML Page" ) ); + + browser.forward(); + QTRY_COMPARE( browser.caption(), QString( "image.png" ) ); + + // Optional: falls urlChanged existiert + if ( !urlChangedSpy.isEmpty() ) + { + QVERIFY( urlChangedSpy.count() >= 2 ); + } +} + +void TestQLiteHtmlBrowserHistory::forwardHistoryIsClearedWhenNavigatingToNewTarget() +{ + const QString image1Path = createTestImage( "image1.png", QSize( 100, 50 ) ); + const QString image2Path = createTestImage( "image2.png", QSize( 200, 100 ) ); + const QUrl htmlUrl = createHtmlPageWithImageLink( "page2.html", "image1.png" ); + const QUrl image1Url = QUrl::fromLocalFile( image1Path ); + const QUrl image2Url = QUrl::fromLocalFile( image2Path ); + + QLiteHtmlBrowser browser; + browser.resize( 800, 600 ); + browser.show(); + QVERIFY( QTest::qWaitForWindowExposed( &browser ) ); + + browser.setSource( htmlUrl ); + QTRY_COMPARE( browser.caption(), QString( "Test HTML Page" ) ); + + browser.setSource( image1Url ); + QTRY_COMPARE( browser.caption(), QString( "image1.png" ) ); + + browser.backward(); + QTRY_COMPARE( browser.caption(), QString( "Test HTML Page" ) ); + + // neue Navigation nach "zurück" muss Forward-History löschen + browser.setSource( image2Url ); + QTRY_COMPARE( browser.caption(), QString( "image2.png" ) ); +} diff --git a/test/library/test_history_image.h b/test/library/test_history_image.h new file mode 100644 index 0000000..8a9b5f0 --- /dev/null +++ b/test/library/test_history_image.h @@ -0,0 +1,30 @@ +#pragma once + +#include +#include +#include +#include +#include +#include + +#include + +class TestQLiteHtmlBrowserHistory : public QObject +{ + Q_OBJECT + +private: + QTemporaryDir mTempDir; + + QString writeFile( const QString& name, const QByteArray& content ); + + QString createTestImage( const QString& name, const QSize& size = QSize( 320, 200 ) ); + + QUrl createHtmlPageWithImageLink( const QString& htmlName, const QString& imageFileName ); + +private slots: + void initTestCase() { QVERIFY( mTempDir.isValid() ); } + + void imageUrlIsAddedToHistory(); + void forwardHistoryIsClearedWhenNavigatingToNewTarget(); +}; diff --git a/test/library/test_html_content.cpp b/test/library/test_html_content.cpp index e782237..be106e3 100644 --- a/test/library/test_html_content.cpp +++ b/test/library/test_html_content.cpp @@ -75,7 +75,7 @@ void HTMLContentTest::test_lists_data()
  • Coffee
  • Tea
  • Milk
  • - + )-"; QTest::newRow( "description list" ) << R"-( @@ -84,7 +84,7 @@ void HTMLContentTest::test_lists_data()
    - black hot drink
    Milk
    - white cold drink
    - + )-"; // litehtml seems to support this via css only @@ -94,7 +94,7 @@ void HTMLContentTest::test_lists_data()
  • Coffee
  • Tea
  • Milk
  • - + )-"; // litehtml seems to support this via css only @@ -104,7 +104,7 @@ void HTMLContentTest::test_lists_data()
  • Coffee
  • Tea
  • Milk
  • - + )-"; QTest::newRow( "unordered list disc image " ) << R"-( @@ -151,7 +151,7 @@ void HTMLContentTest::test_img_data() { QTest::addColumn( "html" ); - QTest::newRow( "Simple local image " ) << R"-( + QTest::newRow( "Simple local image (png) " ) << R"-( @@ -160,6 +160,24 @@ void HTMLContentTest::test_img_data() )-"; + QTest::newRow( "Simple local image (svg) " ) << R"-( + + + + + + + )-"; + + QTest::newRow( "Simple local image (jpg) " ) << R"-( + + + + + + + )-"; + QTest::newRow( "invalid image" ) << R"-( @@ -191,9 +209,19 @@ void HTMLContentTest::test_img_scale_data() )-"; + auto contentSVG = R"-( + + + + + + + )-"; + QTest::newRow( "procitec_logo scale 100% " ) << content << 1.0; QTest::newRow( "procitec_logo scale 150% " ) << content << 1.50; QTest::newRow( "procitec_logo scale 50% " ) << content << 0.50; + QTest::newRow( "plantuml svg scale 50% " ) << contentSVG << 0.50; } void HTMLContentTest::test_img_scale() @@ -213,9 +241,9 @@ void HTMLContentTest::test_tables_data() - +

    HTML Table

    - + @@ -253,7 +281,7 @@ void HTMLContentTest::test_tables_data()
    CompanyItaly
    - + )-"; }