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..282dc08 100644 --- a/src/QLiteHtmlBrowserImpl.cpp +++ b/src/QLiteHtmlBrowserImpl.cpp @@ -12,6 +12,13 @@ #include #include #include +#include +#include +#include +#include +#include +#include +#include QLiteHtmlBrowserImpl::QLiteHtmlBrowserImpl( QWidget* parent ) : QWidget( parent ) @@ -166,20 +173,26 @@ void QLiteHtmlBrowserImpl::mousePressEvent( QMouseEvent* e ) // } //} -void QLiteHtmlBrowserImpl::setUrl( const QUrl& url, int type, bool clearFWHist ) +bool QLiteHtmlBrowserImpl::isImageUrl( const QString& u ) const { - mUrl = UrlType( url, type ); - auto [home_url, home_type] = mHome; - if ( home_url.isEmpty() ) - { - mHome = UrlType( url, type ); - } + const auto lower = u.toLower(); + return lower.endsWith( ".png" ) || lower.endsWith( ".jpg" ) || lower.endsWith( ".jpeg" ) || lower.endsWith( ".gif" ) || lower.endsWith( ".svg" ); +} +bool QLiteHtmlBrowserImpl::isHtmlUrl( const QString& u ) const +{ + const auto lower = u.toLower(); + return lower.endsWith( ".html" ) || lower.endsWith( ".htm" ); +} + +#include +void QLiteHtmlBrowserImpl::setUrl( const QUrl& url, int type, bool clearFWHist ) +{ if ( mContainer ) { auto pure_url = QUrl( url ); pure_url.setFragment( {} ); - QString html; + QByteArray content; if ( pure_url.isLocalFile() ) { @@ -188,20 +201,40 @@ void QLiteHtmlBrowserImpl::setUrl( const QUrl& url, int type, bool clearFWHist ) QFile f( pure_url.toLocalFile() ); if ( f.open( QIODevice::ReadOnly ) ) { - html = f.readAll(); + content = f.readAll(); f.close(); } } else { // eg. if ( url.scheme() == "qthelp" ) - html = mResourceHandler( type, url ); + content = mResourceHandler( type, url ); } - if ( !html.isEmpty() ) + if ( !content.isEmpty() ) { - parseUrl( url ); - mContainer->setHtml( html, url ); + if ( isHtmlUrl( url.toString() ) ) + { + parseUrl( url ); + mContainer->setHtml( QString::fromUtf8( content ), url ); + } + else if ( isImageUrl( url.toString() ) ) + { + onImageClicked( url ); + return; + } + else + { + // could not be shown / displayed -> return + return; + } + + mUrl = UrlType( url, type ); + auto [home_url, home_type] = mHome; + if ( home_url.isEmpty() ) + { + mHome = UrlType( url, type ); + } auto hist_url = QUrl(); @@ -290,18 +323,49 @@ 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() ); + 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 +394,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() ); } @@ -504,3 +569,77 @@ QString QLiteHtmlBrowserImpl::selectedText() const } return text; } + +void QLiteHtmlBrowserImpl::onImageClicked( const QUrl& url ) +{ + auto pure_url = QUrl( url ); + pure_url.setFragment( {} ); + QFileInfo f( url.toLocalFile() ); + + if ( !f.exists() ) + { + return; + } + + // Bild aus Bytes laden + QImage img; + + // sinnvolle Startgröße (z.B. max 80% der Parentgröße) + const QSize parentSize = this->size(); + const int def_w = parentSize.width() * 8 / 10; + const int def_h = parentSize.height() * 8 / 10; + + if ( !img.load( f.absoluteFilePath() ) ) + { + if ( url.toString().toLower().endsWith( ".svg" ) ) + { + img = loadSvgFromFile( f.absoluteFilePath() ); + } + } + + if ( img.isNull() ) + { + return; + } + + const int w = qMax( img.width(), def_w ); + const int h = qMax( img.height(), def_h ); + + // Dialog mit Bild anlegen, Parent ist dieses Widget (MyViewer) + QDialog* dlg = new QDialog( this ); + dlg->setAttribute( Qt::WA_DeleteOnClose ); + dlg->setWindowTitle( f.fileName() ); + + // Inhalt: ScrollArea + QLabel mit Pixmap + auto* layout = new QVBoxLayout( dlg ); + auto* scroll = new QScrollArea( dlg ); + // scroll->setHorizontalScrollBarPolicy( Qt::ScrollBarAlwaysOn ); + // scroll->setVerticalScrollBarPolicy( Qt::ScrollBarAlwaysOn ); + auto* label = new QLabel( dlg ); + + label->setPixmap( QPixmap::fromImage( img ) ); + label->setAlignment( Qt::AlignCenter ); + label->resize( w, h ); + scroll->setWidget( label ); + scroll->setWidgetResizable( true ); + + layout->addWidget( scroll ); + dlg->setLayout( layout ); + + // Optional: Maximalgröße begrenzen (80% des Screens) + QSize screenSize = screen()->availableGeometry().size(); + dlg->setMaximumSize( screenSize * 0.5 ); + + auto margins = dlg->contentsMargins() + scroll->contentsMargins() + scroll->viewport()->contentsMargins() + label->contentsMargins(); + + dlg->resize( qMax( 200, w + scroll->verticalScrollBar()->width() + margins.left() + margins.right() ), + qMax( 200, h + scroll->horizontalScrollBar()->height() + margins.top() + margins.bottom() ) ); + + // Dialog über dem Parent zentrieren (absolute Screen-Koordinaten) + QRect dialogRect = dlg->frameGeometry(); + dialogRect.moveCenter( this->mapToGlobal( this->rect().center() ) ); + dlg->move( dialogRect.topLeft() ); + + dlg->setModal( true ); // optional, je nach gewünschtem Verhalten + dlg->show(); // oder dlg->exec(); +} diff --git a/src/QLiteHtmlBrowserImpl.h b/src/QLiteHtmlBrowserImpl.h index 95b6253..4cd8ee9 100644 --- a/src/QLiteHtmlBrowserImpl.h +++ b/src/QLiteHtmlBrowserImpl.h @@ -111,6 +111,10 @@ class QLiteHtmlBrowserImpl : public QWidget void parseUrl( const QUrl& url ); QString readResourceCss( const QString& ) const; void applyCSS(); + bool isImageUrl( const QString& u ) const; + bool isHtmlUrl( const QString& u ) const; + void onImageClicked( const QUrl& url ); + QImage loadSvgFromFile( const QString& filename ); Q_DISABLE_COPY_MOVE( QLiteHtmlBrowserImpl ); 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/library/CMakeLists.txt b/test/library/CMakeLists.txt index 12d40d4..4c933aa 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}") 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_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
    - + )-"; }