Issue #128: Create list of markup annotations

This commit is contained in:
Jakub Melka 2023-12-24 19:03:28 +01:00
parent 53849f51e1
commit 89ea815696
10 changed files with 685 additions and 52 deletions

View File

@ -189,6 +189,114 @@ PDFAnnotation::PDFAnnotation() :
}
QString PDFAnnotation::getGUICaption() const
{
QStringList texts;
switch (getType())
{
case pdf::AnnotationType::Text:
texts << PDFTranslationContext::tr("Text");
break;
case pdf::AnnotationType::Link:
texts << PDFTranslationContext::tr("Line");
break;
case pdf::AnnotationType::FreeText:
texts << PDFTranslationContext::tr("Free Text");
break;
case pdf::AnnotationType::Line:
texts << PDFTranslationContext::tr("Line");
break;
case pdf::AnnotationType::Square:
texts << PDFTranslationContext::tr("Square");
break;
case pdf::AnnotationType::Circle:
texts << PDFTranslationContext::tr("Circle");
break;
case pdf::AnnotationType::Polygon:
texts << PDFTranslationContext::tr("Polygon");
break;
case pdf::AnnotationType::Polyline:
texts << PDFTranslationContext::tr("Polyline");
break;
case pdf::AnnotationType::Highlight:
texts << PDFTranslationContext::tr("Highlight");
break;
case pdf::AnnotationType::Underline:
texts << PDFTranslationContext::tr("Underline");
break;
case pdf::AnnotationType::Squiggly:
texts << PDFTranslationContext::tr("Squiggly");
break;
case pdf::AnnotationType::StrikeOut:
texts << PDFTranslationContext::tr("Strike Out");
break;
case pdf::AnnotationType::Stamp:
texts << PDFTranslationContext::tr("Stamp");
break;
case pdf::AnnotationType::Caret:
texts << PDFTranslationContext::tr("Caret");
break;
case pdf::AnnotationType::Ink:
texts << PDFTranslationContext::tr("Ink");
break;
case pdf::AnnotationType::Popup:
texts << PDFTranslationContext::tr("Popup");
break;
case pdf::AnnotationType::FileAttachment:
texts << PDFTranslationContext::tr("File Attachment");
break;
case pdf::AnnotationType::Sound:
texts << PDFTranslationContext::tr("Sound");
break;
case pdf::AnnotationType::Movie:
texts << PDFTranslationContext::tr("Movie");
break;
case pdf::AnnotationType::Widget:
texts << PDFTranslationContext::tr("Widget");
break;
case pdf::AnnotationType::Screen:
texts << PDFTranslationContext::tr("Screen");
break;
case pdf::AnnotationType::PrinterMark:
texts << PDFTranslationContext::tr("Printer Mark");
break;
case pdf::AnnotationType::TrapNet:
texts << PDFTranslationContext::tr("Trap Net");
break;
case pdf::AnnotationType::Watermark:
texts << PDFTranslationContext::tr("Watermark");
break;
case pdf::AnnotationType::Redact:
texts << PDFTranslationContext::tr("Redaction");
break;
case pdf::AnnotationType::Projection:
texts << PDFTranslationContext::tr("Projection");
break;
case pdf::AnnotationType::_3D:
texts << PDFTranslationContext::tr("3D");
break;
case pdf::AnnotationType::RichMedia:
texts << PDFTranslationContext::tr("Rich Media");
break;
default:
break;
}
if (isReplyTo())
{
texts << PDFTranslationContext::tr("Reply");
}
if (!getContents().isEmpty())
{
texts << getContents();
}
return texts.join(" | ");
}
void PDFAnnotation::draw(AnnotationDrawParameters& parameters) const
{
Q_UNUSED(parameters);
@ -1634,6 +1742,25 @@ bool PDFAnnotationManager::hasAnyPageAnnotation(const std::vector<PDFInteger>& p
return std::any_of(pageIndices.cbegin(), pageIndices.cend(), std::bind(&PDFAnnotationManager::hasAnnotation, this, std::placeholders::_1));
}
bool PDFAnnotationManager::hasAnyPageAnnotation() const
{
if (!m_document)
{
return false;
}
size_t pageCount = m_document->getCatalog()->getPageCount();
for (size_t i = 0; i < pageCount; ++i)
{
if (hasAnnotation(i))
{
return true;
}
}
return false;
}
PDFFormManager* PDFAnnotationManager::getFormManager() const
{
return m_formManager;

View File

@ -509,6 +509,7 @@ public:
virtual PDFMarkupAnnotation* asMarkupAnnotation() { return nullptr; }
virtual const PDFMarkupAnnotation* asMarkupAnnotation() const { return nullptr; }
virtual bool isReplyTo() const { return false; }
virtual QString getGUICaption() const;
/// Draws the annotation using parameters. Annotation is drawn onto painter,
/// but actual graphics can be drawn outside of annotation's rectangle.
@ -1524,10 +1525,10 @@ public:
/// \param page Page
/// \param[in,out] annotationRectangle Input/output annotation rectangle
QTransform prepareTransformations(const QTransform& pagePointToDevicePointMatrix,
QPaintDevice* device,
const PDFAnnotation::Flags annotationFlags,
const PDFPage* page,
QRectF& annotationRectangle) const;
QPaintDevice* device,
const PDFAnnotation::Flags annotationFlags,
const PDFPage* page,
QRectF& annotationRectangle) const;
/// Returns current appearance stream for given page annotation
/// \param pageAnnotation Page annotation
@ -1549,6 +1550,9 @@ public:
/// Returns true, if any page in the given indices has annotation
bool hasAnyPageAnnotation(const std::vector<PDFInteger>& pageIndices) const;
/// Returns true, if any page in the document has annotation
bool hasAnyPageAnnotation() const;
protected:
void drawWidgetAnnotationHighlight(QRectF annotationRectangle,
const PDFAnnotation* annotation,

View File

@ -95,6 +95,7 @@
<file>resources/book.svg</file>
<file>resources/wallet.svg</file>
<file>resources/web.svg</file>
<file>resources/user.svg</file>
<file>resources/create-bitonal-document.svg</file>
<file>resources/sanitize-document.svg</file>
<file>resources/sidebar-attachment.svg</file>
@ -103,10 +104,13 @@
<file>resources/sidebar-speech.svg</file>
<file>resources/sidebar-thumbnails.svg</file>
<file>resources/sidebar-visibility.svg</file>
<file>resources/sidebar-annotations.svg</file>
<file>resources/outline.svg</file>
<file>resources/sidebar-favourites.svg</file>
<file>resources/bookmark.svg</file>
<file>resources/bookmark-next.svg</file>
<file>resources/bookmark-previous.svg</file>
<file>resources/page.svg</file>
<file>resources/bubble.svg</file>
</qresource>
</RCC>

View File

@ -33,6 +33,7 @@
#include "pdfdocumentbuilder.h"
#include "pdfwidgetutils.h"
#include "pdfbookmarkui.h"
#include "pdfwidgetannotation.h"
#include <QMenu>
#include <QAction>
@ -45,20 +46,14 @@
#include <QHBoxLayout>
#include <QDialog>
#include <QPushButton>
#include <QStandardItemModel>
#include <QSortFilterProxyModel>
#include "pdfdbgheap.h"
namespace pdfviewer
{
constexpr const char* STYLESHEET =
"QPushButton { background-color: #404040; color: #FFFFFF; }"
"QPushButton:disabled { background-color: #404040; color: #000000; }"
"QPushButton:checked { background-color: #808080; color: #FFFFFF; }"
"QWidget#thumbnailsToolbarWidget { background-color: #F0F0F0 }"
"QWidget#speechPage { background-color: #F0F0F0 }"
"QWidget#PDFSidebarWidget { background-color: #404040; background: green;}";
PDFSidebarWidget::PDFSidebarWidget(pdf::PDFDrawWidgetProxy* proxy,
PDFTextToSpeech* textToSpeech,
pdf::PDFCertificateStore* certificateStore,
@ -74,22 +69,32 @@ PDFSidebarWidget::PDFSidebarWidget(pdf::PDFDrawWidgetProxy* proxy,
m_bookmarkManager(bookmarkManager),
m_settings(settings),
m_outlineTreeModel(nullptr),
m_outlineSortProxyTreeModel(nullptr),
m_thumbnailsModel(nullptr),
m_optionalContentTreeModel(nullptr),
m_bookmarkItemModel(nullptr),
m_notesTreeModel(nullptr),
m_notesSortProxyTreeModel(nullptr),
m_document(nullptr),
m_optionalContentActivity(nullptr),
m_attachmentsTreeModel(nullptr)
{
ui->setupUi(this);
setStyleSheet(STYLESHEET);
// Outline
QIcon outlineIcon(":/resources/outline.svg");
m_outlineTreeModel = new pdf::PDFOutlineTreeItemModel(qMove(outlineIcon), editableOutline, this);
ui->outlineTreeView->setModel(m_outlineTreeModel);
m_outlineSortProxyTreeModel = new QSortFilterProxyModel(this);
m_outlineSortProxyTreeModel->setFilterKeyColumn(0);
m_outlineSortProxyTreeModel->setFilterRole(Qt::DisplayRole);
m_outlineSortProxyTreeModel->setAutoAcceptChildRows(false);
m_outlineSortProxyTreeModel->setRecursiveFilteringEnabled(true);
m_outlineSortProxyTreeModel->setFilterCaseSensitivity(Qt::CaseInsensitive);
m_outlineSortProxyTreeModel->setSourceModel(m_outlineTreeModel);
ui->outlineTreeView->setModel(m_outlineSortProxyTreeModel);
ui->outlineTreeView->header()->hide();
connect(ui->outlineSearchLineEdit, &QLineEdit::editingFinished, this, &PDFSidebarWidget::onOutlineSearchText);
connect(ui->outlineSearchLineEdit, &QLineEdit::textChanged, this, &PDFSidebarWidget::onOutlineSearchText);
if (editableOutline)
{
@ -139,6 +144,21 @@ PDFSidebarWidget::PDFSidebarWidget(pdf::PDFDrawWidgetProxy* proxy,
connect(ui->bookmarksView->selectionModel(), &QItemSelectionModel::currentChanged, this, &PDFSidebarWidget::onBookmarsCurrentIndexChanged);
connect(ui->bookmarksView, &QListView::clicked, this, &PDFSidebarWidget::onBookmarkClicked);
// Notes
m_notesTreeModel = new QStandardItemModel(this);
m_notesSortProxyTreeModel = new QSortFilterProxyModel(this);
m_notesSortProxyTreeModel->setFilterKeyColumn(0);
m_notesSortProxyTreeModel->setFilterRole(Qt::DisplayRole);
m_notesSortProxyTreeModel->setAutoAcceptChildRows(false);
m_notesSortProxyTreeModel->setRecursiveFilteringEnabled(true);
m_notesSortProxyTreeModel->setFilterCaseSensitivity(Qt::CaseInsensitive);
m_notesSortProxyTreeModel->setSourceModel(m_notesTreeModel);
ui->notesTreeView->setModel(m_notesSortProxyTreeModel);
ui->notesTreeView->header()->hide();
connect(ui->notesSearchLineEdit, &QLineEdit::editingFinished, this, &PDFSidebarWidget::onNotesSearchText);
connect(ui->notesSearchLineEdit, &QLineEdit::textChanged, this, &PDFSidebarWidget::onNotesSearchText);
connect(ui->notesTreeView, &QTreeView::clicked, this, &PDFSidebarWidget::onNotesItemClicked);
m_pageInfo[Invalid] = { nullptr, ui->emptyPage };
m_pageInfo[OptionalContent] = { ui->optionalContentButton, ui->optionalContentPage };
m_pageInfo[Outline] = { ui->outlineButton, ui->outlinePage };
@ -147,6 +167,7 @@ PDFSidebarWidget::PDFSidebarWidget(pdf::PDFDrawWidgetProxy* proxy,
m_pageInfo[Speech] = { ui->speechButton, ui->speechPage };
m_pageInfo[Signatures] = { ui->signaturesButton, ui->signaturesPage };
m_pageInfo[Bookmarks] = { ui->bookmarksButton, ui->bookmarksPage };
m_pageInfo[Notes] = { ui->notesButton, ui->notesPage };
for (const auto& pageInfo : m_pageInfo)
{
@ -251,6 +272,11 @@ void PDFSidebarWidget::setDocument(const pdf::PDFModifiedDocument& document, con
updateGUI(preferred);
updateButtons();
updateSignatures(signatures);
if (document.hasReset() || document.hasFlag(pdf::PDFModifiedDocument::Annotation))
{
updateNotes();
}
}
bool PDFSidebarWidget::isEmpty() const
@ -294,6 +320,9 @@ bool PDFSidebarWidget::isEmpty(Page page) const
case Signatures:
return m_signatures.empty();
case Notes:
return !m_document || !m_proxy->getAnnotationManager()->hasAnyPageAnnotation();
default:
Q_ASSERT(false);
break;
@ -663,6 +692,120 @@ void PDFSidebarWidget::updateSignatures(const std::vector<pdf::PDFSignatureVerif
ui->signatureTreeWidget->setUpdatesEnabled(true);
}
void PDFSidebarWidget::updateNotes()
{
const bool updatesEnabled = ui->notesTreeView->updatesEnabled();
ui->notesTreeView->setUpdatesEnabled(false);
m_notesTreeModel->clear();
m_markupAnnotations.clear();
if (m_document)
{
QIcon bubbleIcon(":/resources/bubble.svg");
QIcon pageIcon(":/resources/page.svg");
QIcon userIcon(":/resources/user.svg");
pdf::PDFAnnotationManager annotationManager(m_proxy->getFontCache(),
m_proxy->getCMSManager(),
m_optionalContentActivity,
pdf::PDFMeshQualitySettings(),
m_proxy->getFeatures(),
pdf::PDFAnnotationManager::Target::View,
nullptr);
annotationManager.setDocument(pdf::PDFModifiedDocument(const_cast<pdf::PDFDocument*>(m_document), m_optionalContentActivity));
pdf::PDFInteger pageCount = m_document->getCatalog()->getPageCount();
for (pdf::PDFInteger pageIndex = 0; pageIndex < pageCount; ++pageIndex)
{
const pdf::PDFAnnotationManager::PageAnnotations& pageAnnotations = annotationManager.getPageAnnotations(pageIndex);
if (pageAnnotations.isEmpty())
{
continue;
}
std::map<QString, std::vector<const pdf::PDFMarkupAnnotation*>> annotations;
for (const pdf::PDFAnnotationManager::PageAnnotation& pageAnnotation : pageAnnotations.annotations)
{
if (!pageAnnotation.annotation || !pageAnnotation.annotation->asMarkupAnnotation())
{
continue;
}
const pdf::PDFMarkupAnnotation* markupAnnotation = pageAnnotation.annotation->asMarkupAnnotation();
QString user = markupAnnotation->getWindowTitle();
if (user.isEmpty())
{
user = tr("User");
}
annotations[user].push_back(markupAnnotation);
}
if (!annotations.empty())
{
QStandardItem* pageItem = new QStandardItem(pageIcon, tr("Page %1").arg(pageIndex + 1));
pageItem->setFlags(Qt::ItemIsEnabled);
for (const auto& annotationItem : annotations)
{
QStandardItem* userItem = new QStandardItem(userIcon, annotationItem.first);
userItem->setFlags(Qt::ItemIsEnabled);
pageItem->appendRow(userItem);
for (const pdf::PDFMarkupAnnotation* markupAnnotation : annotationItem.second)
{
QStandardItem* annotationTreeItem = new QStandardItem(bubbleIcon, markupAnnotation->getGUICaption());
annotationTreeItem->setData(int(m_markupAnnotations.size()), Qt::UserRole);
annotationTreeItem->setFlags(Qt::ItemIsEnabled | Qt::ItemIsSelectable);
userItem->appendRow(annotationTreeItem);
m_markupAnnotations.push_back(std::make_pair(markupAnnotation->getSelfReference(), pageIndex));
}
}
m_notesTreeModel->appendRow(pageItem);
}
}
}
ui->notesTreeView->setUpdatesEnabled(updatesEnabled);
ui->notesTreeView->expandAll();
}
void PDFSidebarWidget::onOutlineSearchText()
{
QString text = ui->outlineSearchLineEdit->text();
const bool isWildcard = text.contains(QChar('*')) || text.contains(QChar('?'));
if (isWildcard)
{
m_outlineSortProxyTreeModel->setFilterWildcard(text);
}
else
{
m_outlineSortProxyTreeModel->setFilterFixedString(text);
}
}
void PDFSidebarWidget::onNotesSearchText()
{
QString text = ui->notesSearchLineEdit->text();
const bool isWildcard = text.contains(QChar('*')) || text.contains(QChar('?'));
if (isWildcard)
{
m_notesSortProxyTreeModel->setFilterWildcard(text);
}
else
{
m_notesSortProxyTreeModel->setFilterFixedString(text);
}
}
void PDFSidebarWidget::onPageButtonClicked()
{
QObject* pushButton = sender();
@ -680,7 +823,8 @@ void PDFSidebarWidget::onPageButtonClicked()
void PDFSidebarWidget::onOutlineItemClicked(const QModelIndex& index)
{
if (const pdf::PDFAction* action = m_outlineTreeModel->getAction(index))
QModelIndex sourceIndex = m_outlineSortProxyTreeModel->mapToSource(index);
if (const pdf::PDFAction* action = m_outlineTreeModel->getAction(sourceIndex))
{
Q_EMIT actionTriggered(action);
}
@ -785,18 +929,18 @@ void PDFSidebarWidget::onOutlineTreeViewContextMenuRequested(const QPoint& pos)
{
QMenu contextMenu;
QModelIndex index = ui->outlineTreeView->indexAt(pos);
QModelIndex proxyIndex = ui->outlineTreeView->indexAt(pos);
auto onFollow = [this, index]()
auto onFollow = [this, proxyIndex]()
{
onOutlineItemClicked(index);
onOutlineItemClicked(proxyIndex);
};
auto onInsert = [this, index]()
auto onInsert = [this, proxyIndex]()
{
if (index.isValid())
if (proxyIndex.isValid())
{
ui->outlineTreeView->model()->insertRow(index.row() + 1, index.parent());
ui->outlineTreeView->model()->insertRow(proxyIndex.row() + 1, proxyIndex.parent());
}
else
{
@ -804,42 +948,43 @@ void PDFSidebarWidget::onOutlineTreeViewContextMenuRequested(const QPoint& pos)
}
};
auto onDelete = [this, index]()
auto onDelete = [this, proxyIndex]()
{
ui->outlineTreeView->model()->removeRow(index.row(), index.parent());
ui->outlineTreeView->model()->removeRow(proxyIndex.row(), proxyIndex.parent());
};
auto onRename = [this, index]()
auto onRename = [this, proxyIndex]()
{
ui->outlineTreeView->edit(index);
ui->outlineTreeView->edit(proxyIndex);
};
QAction* followAction = contextMenu.addAction(tr("Follow"), onFollow);
followAction->setEnabled(index.isValid());
followAction->setEnabled(proxyIndex.isValid());
contextMenu.addSeparator();
QAction* deleteAction = contextMenu.addAction(tr("Delete"), onDelete);
QAction* insertAction = contextMenu.addAction(tr("Insert"), this, onInsert);
QAction* renameAction = contextMenu.addAction(tr("Rename"), this, onRename);
deleteAction->setEnabled(index.isValid());
deleteAction->setEnabled(proxyIndex.isValid());
insertAction->setEnabled(true);
renameAction->setEnabled(index.isValid());
renameAction->setEnabled(proxyIndex.isValid());
contextMenu.addSeparator();
const pdf::PDFOutlineItem* outlineItem = m_outlineTreeModel->getOutlineItem(index);
QModelIndex sourceIndex = m_outlineSortProxyTreeModel->mapToSource(proxyIndex);
const pdf::PDFOutlineItem* outlineItem = m_outlineTreeModel->getOutlineItem(sourceIndex);
const bool isFontBold = outlineItem && outlineItem->isFontBold();
const bool isFontItalics = outlineItem && outlineItem->isFontItalics();
auto onFontBold = [this, index, isFontBold]()
auto onFontBold = [this, sourceIndex, isFontBold]()
{
m_outlineTreeModel->setFontBold(index, !isFontBold);
m_outlineTreeModel->setFontBold(sourceIndex, !isFontBold);
};
auto onFontItalic = [this, index, isFontItalics]()
auto onFontItalic = [this, sourceIndex, isFontItalics]()
{
m_outlineTreeModel->setFontItalics(index, !isFontItalics);
m_outlineTreeModel->setFontItalics(sourceIndex, !isFontItalics);
};
QAction* fontBoldAction = contextMenu.addAction(tr("Font Bold"), onFontBold);
@ -848,20 +993,20 @@ void PDFSidebarWidget::onOutlineTreeViewContextMenuRequested(const QPoint& pos)
fontItalicAction->setCheckable(true);
fontBoldAction->setChecked(isFontBold);
fontItalicAction->setChecked(isFontItalics);
fontBoldAction->setEnabled(index.isValid());
fontItalicAction->setEnabled(index.isValid());
fontBoldAction->setEnabled(sourceIndex.isValid());
fontItalicAction->setEnabled(sourceIndex.isValid());
QMenu* submenu = new QMenu(tr("Set Target"), &contextMenu);
QAction* targetAction = contextMenu.addMenu(submenu);
targetAction->setEnabled(index.isValid());
targetAction->setEnabled(sourceIndex.isValid());
auto createOnSetTarget = [this, index](pdf::DestinationType destinationType)
auto createOnSetTarget = [this, sourceIndex](pdf::DestinationType destinationType)
{
auto onSetTarget = [this, index, destinationType]()
auto onSetTarget = [this, sourceIndex, destinationType]()
{
pdf::PDFToolManager* toolManager = m_proxy->getWidget()->getToolManager();
auto pickRectangle = [this, index, destinationType](pdf::PDFInteger pageIndex, QRectF rect)
auto pickRectangle = [this, sourceIndex, destinationType](pdf::PDFInteger pageIndex, QRectF rect)
{
pdf::PDFDestination destination;
destination.setDestinationType(destinationType);
@ -872,7 +1017,7 @@ void PDFSidebarWidget::onOutlineTreeViewContextMenuRequested(const QPoint& pos)
destination.setTop(rect.bottom());
destination.setBottom(rect.top());
destination.setZoom(m_proxy->getZoom());
m_outlineTreeModel->setDestination(index, destination);
m_outlineTreeModel->setDestination(sourceIndex, destination);
};
toolManager->pickRectangle(pickRectangle);
@ -881,7 +1026,7 @@ void PDFSidebarWidget::onOutlineTreeViewContextMenuRequested(const QPoint& pos)
return onSetTarget;
};
auto onNamedDestinationTriggered = [this, index]()
auto onNamedDestinationTriggered = [this, sourceIndex]()
{
class SelectNamedDestinationDialog : public QDialog
{
@ -930,7 +1075,7 @@ void PDFSidebarWidget::onOutlineTreeViewContextMenuRequested(const QPoint& pos)
SelectNamedDestinationDialog dialog(items, m_proxy->getWidget());
if (dialog.exec() == QDialog::Accepted)
{
m_outlineTreeModel->setDestination(index, pdf::PDFDestination::createNamed(dialog.selectedText().toLatin1()));
m_outlineTreeModel->setDestination(sourceIndex, pdf::PDFDestination::createNamed(dialog.selectedText().toLatin1()));
}
};
@ -1000,11 +1145,18 @@ void PDFSidebarWidget::onBookmarkClicked(const QModelIndex& index)
}
}
void PDFSidebarWidget::paintEvent(QPaintEvent* event)
void PDFSidebarWidget::onNotesItemClicked(const QModelIndex& index)
{
Q_UNUSED(event);
QPainter painter(this);
painter.fillRect(rect(), QColor(64, 64, 64));
QVariant userData = index.data(Qt::UserRole);
if (userData.isValid())
{
int i = userData.toInt();
if (i >= 0 && i < m_markupAnnotations.size())
{
pdf::PDFInteger pageIndex = m_markupAnnotations[i].second;
m_proxy->goToPage(pageIndex);
}
}
}
} // namespace pdfviewer

View File

@ -27,6 +27,8 @@
class QPushButton;
class QToolButton;
class QWidget;
class QStandardItemModel;
class QSortFilterProxyModel;
namespace Ui
{
@ -69,8 +71,6 @@ public:
QWidget* parent);
virtual ~PDFSidebarWidget() override;
virtual void paintEvent(QPaintEvent* event) override;
enum Page
{
Invalid,
@ -82,6 +82,7 @@ public:
Speech,
Signatures,
Bookmarks,
Notes,
_END
};
@ -110,7 +111,10 @@ private:
void updateGUI(Page preferredPage);
void updateButtons();
void updateSignatures(const std::vector<pdf::PDFSignatureVerificationResult>& signatures);
void updateNotes();
void onOutlineSearchText();
void onNotesSearchText();
void onPageButtonClicked();
void onOutlineItemClicked(const QModelIndex& index);
void onThumbnailsSizeChanged(int size);
@ -122,6 +126,7 @@ private:
void onBookmarkActivated(int index, PDFBookmarkManager::Bookmark bookmark);
void onBookmarsCurrentIndexChanged(const QModelIndex& current, const QModelIndex& previous);
void onBookmarkClicked(const QModelIndex& index);
void onNotesItemClicked(const QModelIndex& index);
struct PageInfo
{
@ -136,15 +141,19 @@ private:
PDFBookmarkManager* m_bookmarkManager;
PDFViewerSettings* m_settings;
pdf::PDFOutlineTreeItemModel* m_outlineTreeModel;
QSortFilterProxyModel* m_outlineSortProxyTreeModel;
pdf::PDFThumbnailsItemModel* m_thumbnailsModel;
pdf::PDFOptionalContentTreeItemModel* m_optionalContentTreeModel;
PDFBookmarkItemModel* m_bookmarkItemModel;
QStandardItemModel* m_notesTreeModel;
QSortFilterProxyModel* m_notesSortProxyTreeModel;
const pdf::PDFDocument* m_document;
pdf::PDFOptionalContentActivity* m_optionalContentActivity;
pdf::PDFAttachmentsTreeItemModel* m_attachmentsTreeModel;
std::map<Page, PageInfo> m_pageInfo;
std::vector<pdf::PDFSignatureVerificationResult> m_signatures;
std::vector<pdf::PDFCertificateInfo> m_certificateInfos;
std::vector<std::pair<pdf::PDFObjectReference, pdf::PDFInteger>> m_markupAnnotations;
bool m_bookmarkChangeInProgress = false;
};

View File

@ -6,8 +6,8 @@
<rect>
<x>0</x>
<y>0</y>
<width>388</width>
<height>681</height>
<width>404</width>
<height>778</height>
</rect>
</property>
<layout class="QHBoxLayout" name="sidebarLayout">
@ -254,6 +254,35 @@
</property>
</widget>
</item>
<item>
<widget class="QToolButton" name="notesButton">
<property name="minimumSize">
<size>
<width>96</width>
<height>0</height>
</size>
</property>
<property name="text">
<string>Notes</string>
</property>
<property name="icon">
<iconset resource="pdf4qtviewer.qrc">
<normaloff>:/resources/sidebar-annotations.svg</normaloff>:/resources/sidebar-annotations.svg</iconset>
</property>
<property name="iconSize">
<size>
<width>64</width>
<height>64</height>
</size>
</property>
<property name="shortcut">
<string>Ctrl+S</string>
</property>
<property name="toolButtonStyle">
<enum>Qt::ToolButtonTextUnderIcon</enum>
</property>
</widget>
</item>
<item>
<spacer name="buttonSpacer">
<property name="orientation">
@ -272,7 +301,7 @@
<item>
<widget class="QStackedWidget" name="stackedWidget">
<property name="currentIndex">
<number>5</number>
<number>6</number>
</property>
<widget class="QWidget" name="emptyPage"/>
<widget class="QWidget" name="outlinePage">
@ -292,6 +321,32 @@
<property name="bottomMargin">
<number>0</number>
</property>
<item>
<layout class="QHBoxLayout" name="outlineSearchLayout">
<property name="leftMargin">
<number>10</number>
</property>
<property name="topMargin">
<number>10</number>
</property>
<property name="rightMargin">
<number>10</number>
</property>
<property name="bottomMargin">
<number>10</number>
</property>
<item>
<widget class="QLineEdit" name="outlineSearchLineEdit">
<property name="placeholderText">
<string>Search text or wildcard...</string>
</property>
<property name="clearButtonEnabled">
<bool>true</bool>
</property>
</widget>
</item>
</layout>
</item>
<item>
<widget class="QTreeView" name="outlineTreeView"/>
</item>
@ -438,6 +493,56 @@
</item>
</layout>
</widget>
<widget class="QWidget" name="notesPage">
<layout class="QVBoxLayout" name="notesPageLayout">
<property name="spacing">
<number>0</number>
</property>
<property name="leftMargin">
<number>0</number>
</property>
<property name="topMargin">
<number>0</number>
</property>
<property name="rightMargin">
<number>0</number>
</property>
<property name="bottomMargin">
<number>0</number>
</property>
<item>
<widget class="QWidget" name="widget" native="true">
<layout class="QVBoxLayout" name="notesSearchLayout">
<property name="leftMargin">
<number>10</number>
</property>
<property name="topMargin">
<number>10</number>
</property>
<property name="rightMargin">
<number>10</number>
</property>
<property name="bottomMargin">
<number>10</number>
</property>
<item>
<widget class="QLineEdit" name="notesSearchLineEdit">
<property name="placeholderText">
<string>Search text or wildcard...</string>
</property>
<property name="clearButtonEnabled">
<bool>true</bool>
</property>
</widget>
</item>
</layout>
</widget>
</item>
<item>
<widget class="QTreeView" name="notesTreeView"/>
</item>
</layout>
</widget>
<widget class="QWidget" name="speechPage">
<layout class="QVBoxLayout" name="verticalLayout_2">
<item>

View File

@ -0,0 +1,58 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
version="1.1"
id="svg815"
width="512"
height="512"
viewBox="0 0 512 512"
sodipodi:docname="bubble.svg"
inkscape:version="0.92.4 (5da689c313, 2019-01-14)">
<metadata
id="metadata821">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
<dc:title />
</cc:Work>
</rdf:RDF>
</metadata>
<defs
id="defs819" />
<sodipodi:namedview
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1"
objecttolerance="10"
gridtolerance="10"
guidetolerance="10"
inkscape:pageopacity="0"
inkscape:pageshadow="2"
inkscape:window-width="3840"
inkscape:window-height="2035"
id="namedview817"
showgrid="false"
inkscape:zoom="3.4335938"
inkscape:cx="150.86235"
inkscape:cy="256"
inkscape:window-x="-13"
inkscape:window-y="-13"
inkscape:window-maximized="1"
inkscape:current-layer="svg815" />
<path
style="fill:#000000;stroke-width:12;stroke:#000000;stroke-opacity:1;stroke-miterlimit:4;stroke-dasharray:none"
d="m 123.75,430.79874 c -1.89158,-0.83496 -2.99805,-2.07362 -4.0049,-4.48334 -0.72128,-1.72628 -0.72276,-2.33532 -0.0163,-6.7317 0.42719,-2.65853 1.20779,-7.7587 1.73467,-11.3337 0.52688,-3.575 1.98151,-13.1375 3.23251,-21.25 1.251,-8.1125 3.05693,-20.36244 4.01318,-27.22208 l 1.73864,-12.47208 -4.40969,-6.52792 C 115.27198,324.84023 107.54774,309.75923 103.10358,296 c -6.25381,-19.36193 -8.066965,-29.81667 -8.45466,-48.75 -0.336065,-16.41179 0.88088,-29.37875 3.91823,-41.75 5.98814,-24.38995 17.17364,-46.76015 32.84264,-65.68295 5.0251,-6.0686 15.79673,-17.0453 21.14297,-21.54549 5.07023,-4.26785 16.86135,-12.5361 23.11796,-16.21093 18.74709,-11.01114 41.75064,-18.834225 62.59842,-21.28858 8.46167,-0.99617 26.90797,-1.016235 35.23392,-0.03835 45.2291,5.312295 87.05443,30.28592 113.99148,68.06356 13.18538,18.49172 22.80239,40.31706 27.2316,61.80081 6.6758,32.38074 2.73273,68.87128 -10.71408,99.15193 -12.77959,28.77816 -33.52708,53.61451 -58.51206,70.04345 -22.11027,14.53867 -43.57104,22.67606 -70,26.54226 -8.71057,1.27424 -30.12062,1.27404 -38.75,-3.6e-4 -13.58803,-2.00671 -26.36472,-5.52709 -39.63815,-10.92159 -4.89192,-1.98813 -8.98733,-3.37666 -9.44328,-3.2017 -0.94968,0.36443 -33.47182,22.15481 -46.41857,31.10123 -4.95,3.42053 -9.83479,6.66185 -10.85508,7.20293 -2.13439,1.1319 -4.49345,1.2322 -6.64492,0.28252 z M 151.28045,399.625 c 8.40229,-5.70625 18.35773,-12.48195 22.12321,-15.0571 12.81859,-8.76643 13.61635,-8.98482 20.4656,-5.60258 12.20829,6.02859 29.60537,11.35241 43.44875,13.2961 7.01506,0.98495 29.69754,1.13786 36.93199,0.24897 10.2186,-1.25555 23.1936,-4.53936 33.52657,-8.48515 5.26643,-2.01106 17.91766,-8.22219 22.97343,-11.27882 6.52901,-3.94732 15.89889,-10.84538 15.26437,-11.23754 -0.38193,-0.23604 -0.18756,-0.60966 0.58079,-1.11639 0.63516,-0.41889 3.40484,-2.85437 6.15484,-5.41217 2.75,-2.55781 6.27445,-5.71147 7.83209,-7.00813 8.60011,-7.15914 20.6777,-23.84493 27.66617,-38.22219 3.23461,-6.65451 8.58328,-20.09247 8.99568,-22.6007 0.64481,-3.92172 1.26965,-6.76993 2.28141,-10.3993 3.03127,-10.87379 4.83812,-27.81403 4.27301,-40.0619 -1.19421,-25.8822 -9.98436,-53.09135 -24.1081,-74.62456 C 365.97447,141.1523 352.64378,127.34519 334.43043,115.18618 327.79221,110.75459 317.21366,105.03773 310.5,102.2537 297.9373,97.04419 283.66999,93.43631 270,92.01218 c -6.11697,-0.63726 -22.38304,-0.63726 -28.5,0 -26.9942,2.81224 -52.03234,12.96726 -74,30.01307 -6.39422,4.9616 -18.15774,16.53065 -23.36093,22.97475 -15.70185,19.44656 -27.74239,44.71965 -32.86574,68.98514 -4.21743,19.97477 -3.79255,46.85768 0.98014,62.01486 0.64943,2.0625 1.55336,5.55 2.00871,7.75 3.03898,14.68243 13.01727,34.76904 25.41296,51.15704 2.01474,2.66363 3.92454,5.33442 4.24401,5.93509 1.25775,2.36486 0.89087,5.80329 -4.9518,46.40787 -1.66195,11.55 -3.13716,21.39375 -3.27823,21.875 -0.14108,0.48125 -0.12804,0.875 0.029,0.875 0.15701,0 7.16007,-4.66875 15.56235,-10.375 z"
id="path825"
inkscape:connector-curvature="0" />
</svg>

After

Width:  |  Height:  |  Size: 4.5 KiB

View File

@ -0,0 +1,58 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
version="1.1"
id="svg815"
width="1024"
height="1024"
viewBox="0 0 1024 1024"
sodipodi:docname="thumbnail.svg"
inkscape:version="0.92.4 (5da689c313, 2019-01-14)">
<metadata
id="metadata821">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
<dc:title></dc:title>
</cc:Work>
</rdf:RDF>
</metadata>
<defs
id="defs819" />
<sodipodi:namedview
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1"
objecttolerance="10"
gridtolerance="10"
guidetolerance="10"
inkscape:pageopacity="0"
inkscape:pageshadow="2"
inkscape:window-width="3840"
inkscape:window-height="2035"
id="namedview817"
showgrid="false"
inkscape:zoom="1.7167969"
inkscape:cx="512"
inkscape:cy="512"
inkscape:window-x="-13"
inkscape:window-y="-13"
inkscape:window-maximized="1"
inkscape:current-layer="svg815" />
<path
style="fill:#000000"
d="m 276.5,830.60545 c -12.46403,-3.95518 -21.57692,-13.08616 -27.29092,-27.34509 L 246.5,796.5 246.22596,509 c -0.18955,-198.85427 0.0529,-289.65833 0.78639,-294.5 2.0117,-13.27951 9.45813,-24.82628 20.12556,-31.20766 C 278.13403,176.71435 271.82056,177 406.2101,177 H 528 v 34 34 l -39.25,0.0508 c -21.5875,0.028 -68.5,0.14045 -104.25,0.25 l -65,0.19917 -0.2519,259.25 -0.2519,259.25 H 515.9981 713 V 599.65372 435.30746 l 19.57694,-0.69657 c 15.87658,-0.5649 20.55473,-1.06922 24.75,-2.6681 7.34954,-2.80104 13.45748,-7.62612 20.26411,-16.00803 3.24992,-4.00206 6.35895,-7.42463 6.90895,-7.6057 0.66553,-0.21911 0.83276,65.31835 0.5,195.94217 l -0.5,196.27139 -3.4196,6.94589 c -4.55071,9.24342 -11.17144,15.69847 -20.83358,20.31226 L 752.5,831.5 l -236,0.18738 c -188.36178,0.14956 -236.80743,-0.0688 -240,-1.08193 z m 102,-114.47858 c -7.03347,-3.24315 -10.51163,-9.32943 -10.48355,-18.34475 0.0204,-6.53417 3.06205,-12.33496 7.98411,-15.22639 3.47762,-2.04291 4.34103,-2.05753 138.49944,-2.34376 100.88716,-0.21525 136.07745,0.008 139.26394,0.88386 5.62495,1.54596 9.72356,6.28724 11.13855,12.8851 1.93536,9.02427 -0.58867,16.50913 -7.22563,21.42712 L 654.85372,717.5 518.67686,717.73564 C 388.16501,717.96147 382.3336,717.89454 378.5,716.12687 Z m -0.0786,-65.0232 c -7.46386,-3.38992 -10.93926,-10.26753 -10.21914,-20.22308 0.4544,-6.28208 2.27183,-9.6521 7.18271,-13.3188 2.75376,-2.05609 3.14225,-2.06249 140.43821,-2.31599 129.55014,-0.2392 137.88253,-0.15066 141.16175,1.5 5.45102,2.74387 7.97887,8.22908 7.99862,17.35632 0.0199,9.19654 -2.13498,13.20327 -8.95351,16.64788 L 651.57622,653 517.03811,652.978 C 388.23376,652.957 382.32618,652.87709 378.4214,651.10363 Z m 1.2624,-64.54508 c -2.0989,-0.75263 -4.74996,-2.19871 -5.89123,-3.21351 -5.18795,-4.61303 -7.42647,-16.59064 -4.41573,-23.6272 0.75726,-1.76983 2.77973,-4.53715 4.49438,-6.14958 2.99437,-2.81588 3.64124,-2.96669 16.37316,-3.81705 7.29059,-0.48693 69.52195,-0.69145 138.29191,-0.45449 112.80261,0.3887 125.34301,0.58946 128.17116,2.05196 4.15474,2.14849 6.97832,6.28694 8.2935,12.15553 2.10039,9.37234 -0.97949,17.81496 -8.00435,21.94168 -3.47563,2.04175 -4.30628,2.05535 -138.4966,2.26758 -112.10884,0.17731 -135.64709,-0.0185 -138.8162,-1.15492 z m -2.65698,-66.79816 c -12.20293,-6.21356 -12.23046,-27.37613 -0.0436,-33.52094 l 4.48321,-2.2605 136.09023,0.2605 136.09022,0.26051 3.62328,2.48282 c 5.47291,3.75027 7.98155,8.99181 8.08468,16.89206 0.1005,7.69825 -1.65711,11.77806 -6.58974,15.29628 L 655.5,521.5 518.48306,521.76043 381.46612,522.02087 Z M 568.45863,402.12034 c -4.18992,-1.90322 -8.81174,-6.63494 -10.807,-11.06397 C 556.817,389.20366 556.36237,359.66656 556,283.74983 c -0.275,-57.61259 -0.45738,-104.75009 -0.40528,-104.75 0.0521,1e-4 19.85209,20.35654 44,45.23655 24.1479,24.88001 74.00849,75.50501 110.80131,112.5 L 777.29206,404 674.89603,403.978 c -97.18104,-0.0208 -102.60186,-0.11546 -106.4374,-1.8577 z m 180.7125,-67.46306 -35.29224,-35.34273 0.0884,-27.15727 L 714.05569,245 H 687.7723 661.48892 l -33.98897,-34 -33.98897,-34 h 60.60291 60.60292 L 749.85838,212.49855 785,247.9971 v 59.41966 c 0,32.68082 0.27314,60.13147 0.60699,61.00145 0.33384,0.86999 0.2131,1.58179 -0.26832,1.58179 -0.48142,0 -16.75681,-15.90423 -36.16754,-35.34272 z"
id="path825"
inkscape:connector-curvature="0" />
</svg>

After

Width:  |  Height:  |  Size: 4.8 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 7.0 KiB

View File

@ -0,0 +1,58 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
version="1.1"
id="svg815"
width="1024"
height="1024"
viewBox="0 0 1024 1024"
sodipodi:docname="user.svg"
inkscape:version="0.92.4 (5da689c313, 2019-01-14)">
<metadata
id="metadata821">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
<dc:title></dc:title>
</cc:Work>
</rdf:RDF>
</metadata>
<defs
id="defs819" />
<sodipodi:namedview
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1"
objecttolerance="10"
gridtolerance="10"
guidetolerance="10"
inkscape:pageopacity="0"
inkscape:pageshadow="2"
inkscape:window-width="3840"
inkscape:window-height="2035"
id="namedview817"
showgrid="false"
inkscape:zoom="0.60697936"
inkscape:cx="185.4828"
inkscape:cy="275.57688"
inkscape:window-x="-13"
inkscape:window-y="-13"
inkscape:window-maximized="1"
inkscape:current-layer="svg815" />
<path
style="fill:#000000"
d="M 477,864.46239 C 390.62272,853.65835 314.31776,815.28595 260.86218,755.77054 l -7.84063,-8.72947 0.56752,-11.77053 c 0.9619,-19.94996 4.54833,-32.14942 13.40856,-45.60998 12.25458,-18.61729 21.49165,-25.20954 53.50237,-38.18325 7.7,-3.12075 24.35,-9.85785 37,-14.97134 12.65,-5.11348 31.49625,-12.73545 41.88057,-16.9377 10.38431,-4.20226 18.96608,-7.51173 19.07061,-7.35437 0.10452,0.15735 1.19684,2.9861 2.42738,6.2861 25.52887,68.46232 126.12851,81.62147 171.65688,22.45394 5.29126,-6.87639 11.79687,-18.74728 13.5385,-24.70394 0.68345,-2.3375 1.3375,-4.25 1.45344,-4.25 0.11594,0 5.55721,2.19723 12.09171,4.88273 6.5345,2.6855 30.55591,12.39456 53.38091,21.57569 22.825,9.18112 45.15347,18.40204 49.61883,20.49093 30.47061,14.25411 47.10908,40.07837 49.04731,76.12546 0.71485,13.29473 1.1592,12.11721 -8.66614,22.96545 -49.86235,55.05347 -122.23383,92.5121 -203.0221,105.08186 -17.82643,2.7736 -65.37802,3.54165 -82.9779,1.34027 z M 204.23723,677.75 c -9.6555,-11.97767 -29.47661,-69.04288 -36.21033,-104.25 -14.09404,-73.69037 -0.52683,-165.53835 32.94016,-223 23.15044,-39.74849 38.69845,-60.65806 64.69176,-87 C 336.40763,191.80228 441.47767,151.26628 536.5,159.00952 c 105.41805,8.59037 199.23031,62.59515 262.61934,151.18172 60.29711,84.26561 78.84672,183.98808 53.25696,286.30876 -7.7814,31.11395 -22.80566,70.83445 -30.60325,80.90755 -1.46002,1.8861 -11.84338,-5.11245 -37.96117,-25.58643 l -3.68811,-2.89115 4.04141,-8.71499 C 849.5045,499.31585 803.99895,340.67549 673.61553,254.81938 570.28252,186.77568 441.2721,190.57087 339.28862,264.65452 236.6269,339.23086 191.31249,461.31047 220.93723,583.5 c 4.8048,19.81777 14.85726,47.17324 22.12008,60.19472 l 2.89739,5.19472 -20.01244,15.05528 C 203.75609,680.63528 205.6117,679.45503 204.23723,677.75 Z m 299.49471,-28.78323 c -33.98165,-4.04186 -60.61025,-32.00219 -60.71458,-63.75099 -0.0172,-5.23716 -0.0775,-5.33949 -6.76736,-11.48815 -18.64024,-17.13224 -29.02389,-31.53089 -39.2459,-54.4209 l -4.67746,-10.47419 -6.47114,-2.17773 c -35.29537,-11.87797 -49.05986,-54.4177 -24.91538,-77.00203 l 4.95756,-4.63722 -0.94305,-5.75778 c -0.51867,-3.16678 -0.94565,-12.45615 -0.94884,-20.64305 -0.0374,-96.03281 95.30614,-166.24917 189.99421,-139.9225 7.34733,2.04282 7.34003,2.02994 5.54362,9.77594 -9.93403,42.83467 -49.83637,79.67943 -98.54362,90.99253 -11.73506,2.72567 -11.50692,3.15338 -6.52303,-12.22919 3.72794,-11.50613 10.09719,-23.64226 16.22872,-30.92258 7.52962,-8.94036 7.43637,-9.05493 -2.93121,-3.60165 -29.12931,15.32183 -53.70041,41.77492 -64.30278,69.22796 -4.85895,12.5814 -5.38413,11.74797 7.92698,12.57968 77.20276,4.82381 156.83838,-49.0936 176.21366,-119.30571 0.244,-0.8842 2.17237,-0.13101 6.12193,2.39112 46.95021,29.98177 73.94555,85.2373 67.32809,137.81079 l -1.22405,9.72467 4.97041,5.25548 c 24.18543,25.57252 9.8576,65.5502 -27.46565,76.63496 -3.46794,1.02995 -4.44194,1.91738 -5.87651,5.35417 -11.00733,26.37032 -23.99662,45.15244 -41.55282,60.08411 l -8.91374,7.5812 -0.0119,8.22715 c -0.0535,37.07907 -36.31143,65.56399 -77.25619,60.69391 z"
id="path825"
inkscape:connector-curvature="0" />
</svg>

After

Width:  |  Height:  |  Size: 4.6 KiB