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..b188210 100644 --- a/src/QLiteHtmlBrowserImpl.cpp +++ b/src/QLiteHtmlBrowserImpl.cpp @@ -2,6 +2,7 @@ #include "container_qt.h" #include +#include #include #include #include @@ -12,6 +13,141 @@ #include #include #include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace +{ + +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"; +} + +} // namespace QLiteHtmlBrowserImpl::QLiteHtmlBrowserImpl( QWidget* parent ) : QWidget( parent ) @@ -27,10 +163,24 @@ QLiteHtmlBrowserImpl::QLiteHtmlBrowserImpl( QWidget* parent ) connect( mContainer, &container_qt::scaleChanged, this, &QLiteHtmlBrowserImpl::scaleChanged ); connect( mContainer, &container_qt::selectionChanged, this, &QLiteHtmlBrowserImpl::selectionChanged ); + mImageScroll = new QScrollArea( this ); + mImageScroll->setWidgetResizable( false ); + mImageScroll->setAlignment( Qt::AlignCenter ); + mImageLabel = new QLabel( mImageScroll ); + mImageLabel->setAlignment( Qt::AlignCenter ); + mImageLabel->setSizePolicy( QSizePolicy::Fixed, QSizePolicy::Fixed ); + mImageLabel->installEventFilter( this ); + mImageScroll->setWidget( mImageLabel ); + 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,6 +308,34 @@ void QLiteHtmlBrowserImpl::mousePressEvent( QMouseEvent* e ) e->ignore(); } +void QLiteHtmlBrowserImpl::resizeEvent( QResizeEvent* ev ) +{ + QWidget::resizeEvent( ev ); + + if ( mViewStack && mImageScroll && mViewStack->currentWidget() == mImageScroll && mImageFitToView ) + { + updateImageView(); + } +} + +bool QLiteHtmlBrowserImpl::eventFilter( QObject* watched, QEvent* event ) +{ + if ( watched == mImageLabel && event ) + { + if ( event->type() == QEvent::MouseButtonRelease ) + { + auto* mouseEvent = static_cast( event ); + if ( mouseEvent->button() == Qt::LeftButton && !mCurrentImagePixmap.isNull() ) + { + toggleImageZoomMode(); + return true; + } + } + } + + return QWidget::eventFilter( watched, event ); +} + // void QLiteHtmlBrowser::resizeEvent( QResizeEvent* ev ) //{ // if ( ev ) @@ -166,63 +344,231 @@ void QLiteHtmlBrowserImpl::mousePressEvent( QMouseEvent* e ) // } //} +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 ); - auto [home_url, home_type] = mHome; - if ( home_url.isEmpty() ) + if ( !mContainer ) { - mHome = UrlType( url, type ); + return; } - if ( mContainer ) - { - auto pure_url = QUrl( url ); - pure_url.setFragment( {} ); - QString html; + QUrl pureUrl( url ); + pureUrl.setFragment( {} ); - if ( pure_url.isLocalFile() ) + QByteArray content; + + if ( pureUrl.isLocalFile() ) + { + QFile f( pureUrl.toLocalFile() ); + if ( f.open( QIODevice::ReadOnly ) ) { - // get the content of the url and display the html + 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 ); - QFile f( pure_url.toLocalFile() ); - if ( f.open( QIODevice::ReadOnly ) ) + // optional: falls type Unknown ist und dein Handler den Typ braucht: + if ( content.isEmpty() && type == static_cast( Browser::ResourceType::Unknown ) ) + { + if ( hasHtmlExtension( pureUrl ) ) { - html = f.readAll(); - f.close(); + 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 ); } } - else + } + + if ( content.isEmpty() ) + { + // emit urlChanged( url ); + 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 ) ) { - // eg. if ( url.scheme() == "qthelp" ) - html = mResourceHandler( type, url ); + return; } + } + else + { + return; + } - if ( !html.isEmpty() ) - { - parseUrl( url ); - mContainer->setHtml( html, url ); + const int storedType = isHtml ? static_cast( Browser::ResourceType::Html ) : isImage ? static_cast( Browser::ResourceType::Image ) : type; - auto hist_url = QUrl(); + mUrl = UrlType( url, storedType ); - if ( !mBWHistStack.isEmpty() ) - { - hist_url = mBWHistStack.top().url; - } + auto [home_url, home_type] = mHome; + if ( home_url.isEmpty() ) + { + mHome = UrlType( url, storedType ); + } + + QUrl hist_url; + if ( !mBWHistStack.isEmpty() ) + { + hist_url = mBWHistStack.top().url; + } + + if ( hist_url != url ) + { + mBWHistStack.push( { url, storedType, caption() } ); + } + + 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 ) ); +} - if ( hist_url != url ) +bool QLiteHtmlBrowserImpl::imageFitsViewport( const QSize& imageSize ) const +{ + const QSize viewportSize = imageViewportSize(); + return imageSize.width() <= viewportSize.width() && imageSize.height() <= viewportSize.height(); +} + +void QLiteHtmlBrowserImpl::updateImageView() +{ + if ( !mImageLabel || !mImageScroll || mCurrentImagePixmap.isNull() ) + { + return; + } + + QPixmap displayPixmap = mCurrentImagePixmap; + if ( mImageFitToView ) + { + const QSize viewportSize = imageViewportSize(); + displayPixmap = mCurrentImagePixmap.scaled( viewportSize, Qt::KeepAspectRatio, Qt::SmoothTransformation ); + } + + mImageLabel->setPixmap( displayPixmap ); + mImageLabel->resize( displayPixmap.size() ); + mImageLabel->setMinimumSize( displayPixmap.size() ); + mImageLabel->setMaximumSize( displayPixmap.size() ); + mImageScroll->horizontalScrollBar()->setValue( 0 ); + mImageScroll->verticalScrollBar()->setValue( 0 ); +} + +void QLiteHtmlBrowserImpl::toggleImageZoomMode() +{ + if ( mCurrentImagePixmap.isNull() || imageFitsViewport( mCurrentImagePixmap.size() ) ) + { + return; + } + + mImageFitToView = !mImageFitToView; + updateImageView(); +} + +bool QLiteHtmlBrowserImpl::showImageFromData( const QUrl& url, const QByteArray& imageData ) +{ + if ( !mImageLabel || !mImageScroll ) + { + return false; + } + + QImage img; + if ( !imageData.isEmpty() ) + { + img.loadFromData( imageData ); + if ( img.isNull() && ( isSvgUrl( url ) || looksLikeSvgData( imageData ) ) ) + { + img = loadSvgFromData( imageData ); + } + } + + if ( img.isNull() && url.isLocalFile() ) + { + QFileInfo f( url.toLocalFile() ); + if ( f.exists() ) + { + if ( !img.load( f.absoluteFilePath() ) && isSvgUrl( url ) ) { - mBWHistStack.push( { url, type, mContainer->caption() } ); + img = loadSvgFromFile( f.absoluteFilePath() ); } + } + } - if ( clearFWHist ) - mFWHistStack.clear(); + if ( img.isNull() ) + { + return false; + } - update(); - } + mCurrentImagePixmap = QPixmap::fromImage( img ); - emit urlChanged( url ); + QFileInfo info( normalizedUrlPath( url ) ); + mCurrentCaption = info.fileName().isEmpty() ? url.fileName() : info.fileName(); + if ( mCurrentCaption.isEmpty() ) + { + mCurrentCaption = url.toDisplayString(); } + + showImageView(); + + mImageFitToView = !imageFitsViewport( mCurrentImagePixmap.size() ); + updateImageView(); + + // A second deferred update avoids using a stale viewport size directly after + // switching the stacked widget page. + QTimer::singleShot( 0, this, + [this]() + { + if ( mViewStack && mImageScroll && mViewStack->currentWidget() == mImageScroll && !mCurrentImagePixmap.isNull() ) + { + if ( mImageFitToView ) + { + updateImageView(); + } + } + } ); + + return true; } QUrl QLiteHtmlBrowserImpl::baseUrl( const QUrl& url ) const @@ -243,6 +589,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 +638,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 +734,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() ); } @@ -458,7 +863,7 @@ void QLiteHtmlBrowserImpl::reload() const QString& QLiteHtmlBrowserImpl::caption() const { - return mContainer->caption(); + return mCurrentCaption; } void QLiteHtmlBrowserImpl::print( QPagedPaintDevice* printer ) const @@ -504,3 +909,44 @@ QString QLiteHtmlBrowserImpl::selectedText() const } return text; } + +void QLiteHtmlBrowserImpl::showHtmlView() +{ + if ( mViewStack && mContainer ) + { + mViewStack->setCurrentWidget( mContainer ); + } +} + +void QLiteHtmlBrowserImpl::showImageView() +{ + if ( mViewStack && mImageScroll ) + { + mViewStack->setCurrentWidget( mImageScroll ); + + // When switching from HTML to image view, the scroll area's viewport size can + // still reflect the previously hidden state. Re-apply the scaling once the + // layout has settled so fit-to-view uses the final viewport size. + if ( !mCurrentImagePixmap.isNull() && mImageFitToView ) + { + QTimer::singleShot( 0, this, [this]() { 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..90b0e57 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 @@ -75,6 +78,8 @@ class QLiteHtmlBrowserImpl : public QWidget protected: void changeEvent( QEvent* ) override; void mousePressEvent( QMouseEvent* ) override; + void resizeEvent( QResizeEvent* ) override; + bool eventFilter( QObject* watched, QEvent* event ) override; Q_SIGNALS: /// emited when the url changed due to user interaction, e.g. link activation @@ -106,23 +111,41 @@ 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; + 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; + QPixmap mCurrentImagePixmap = {}; + 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..26308ee 100644 --- a/test/browser/testbrowser.cpp +++ b/test/browser/testbrowser.cpp @@ -72,6 +72,20 @@ TestBrowser::TestBrowser() } } ); + mActHome = new QAction( style()->standardIcon( QStyle::SP_DirHomeIcon ), tr( "home" ), this ); + connect( mActHome, &QAction::triggered, this, [this]() { home(); } ); + mToolBar.addAction( mActHome ); + + mActBackward = new QAction( style()->standardIcon( QStyle::QStyle::SP_ArrowBack ), tr( "backward" ), this ); + connect( mActBackward, &QAction::triggered, this, [this]() { backward(); } ); + mToolBar.addAction( mActBackward ); + + mActForward = new QAction( style()->standardIcon( QStyle::QStyle::SP_ArrowForward ), 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, @@ -244,3 +258,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
    - + )-"; }