wireshark/ui/qt/packet_diagram.cpp

737 lines
24 KiB
C++

/* packet_diagram.cpp
*
* Wireshark - Network traffic analyzer
* By Gerald Combs <gerald@wireshark.org>
* Copyright 1998 Gerald Combs
*
* SPDX-License-Identifier: GPL-2.0-or-later
*/
#include "packet_diagram.h"
#include "math.h"
#include "epan/epan.h"
#include "epan/epan_dissect.h"
#include "wsutil/utf8_entities.h"
#include "wireshark_application.h"
#include "ui/qt/main_window.h"
#include "ui/qt/utils/proto_node.h"
#include "ui/qt/utils/variant_pointer.h"
#include <QContextMenuEvent>
#include <QGraphicsItem>
#include <QMenu>
#if defined(QT_SVG_LIB) && 0
#include <QBuffer>
#include <QMimeData>
#include <QSvgGenerator>
#endif
#include <QDebug>
// "rems" are root em widths, aka the regular font height, similar to rems in CSS.
class DiagramLayout {
public:
DiagramLayout() :
bits_per_row_(32),
small_font_rems_(0.75),
bit_width_rems_(1.0),
padding_rems_(0.5),
span_mark_offset_rems_(0.2),
show_fields_(false)
{
setFont(wsApp->font());
}
void setFont(QFont font) {
regular_font_ = font;
small_font_ = font;
small_font_.setPointSize(regular_font_.pointSize() * small_font_rems_);
QFontMetrics fm(regular_font_);
root_em_ = fm.height();
}
void setShowFields(bool show_fields = false) { show_fields_ = show_fields; }
int bitsPerRow() const { return bits_per_row_; }
const QFont regularFont() const { return regular_font_; }
const QFont smallFont() const { return small_font_; }
int bitWidth() const { return root_em_ * bit_width_rems_; }
int lineHeight() const { return root_em_; }
int hPadding() const { return root_em_ * padding_rems_; }
int vPadding() const { return root_em_ * padding_rems_; }
int spanMarkOffset() const { return root_em_ * span_mark_offset_rems_; }
int rowHeight() const {
int rows = show_fields_ ? 2 : 1;
return ((lineHeight() * rows) + (vPadding() * 2));
}
bool showFields() const { return show_fields_; }
private:
int bits_per_row_;
double small_font_rems_;
double bit_width_rems_;
double padding_rems_;
double span_mark_offset_rems_; // XXX Make this padding_rems_ / 2 instead?
bool show_fields_;
QFont regular_font_;
QFont small_font_;
int root_em_;
};
class FieldInformationGraphicsItem : public QGraphicsPolygonItem
{
public:
FieldInformationGraphicsItem(field_info *fi, int start_bit, int fi_length, const DiagramLayout *layout, QGraphicsItem *parent = nullptr) :
QGraphicsPolygonItem(QPolygonF(), parent),
finfo_(new FieldInformation(fi)),
start_bit_(start_bit),
layout_(layout),
collapsed_len_(fi_length),
collapsed_row_(-1)
{
Q_ASSERT(layout_);
for (int idx = 0; idx < NumSpanMarks; idx++) {
span_marks_[idx] = new QGraphicsLineItem(this);
span_marks_[idx]->hide();
}
int bits_per_row = layout_->bitsPerRow();
int row1_start = start_bit_ % bits_per_row;
int bits_remain = fi_length;
int row1_bits = bits_remain;
if (bits_remain + row1_start > bits_per_row) {
row1_bits = bits_per_row - row1_start;
bits_remain -= row1_bits;
if (row1_start == 0 && bits_remain > bits_per_row) {
// Collapse first row
bits_remain %= bits_per_row;
collapsed_row_ = 0;
}
} else {
bits_remain = 0;
}
int row2_bits = bits_remain;
if (bits_remain > bits_per_row) {
row2_bits = bits_per_row;
bits_remain -= bits_per_row;
if (bits_remain > bits_per_row) {
// Collapse second row
bits_remain %= bits_per_row;
collapsed_row_ = 1;
}
} else {
bits_remain = 0;
}
int row3_bits = bits_remain;
collapsed_len_ = row1_bits + row2_bits + row3_bits;
QRectF rr1, rr2, rr3;
QRectF row_rect = QRectF(row1_start, 0, row1_bits, 1);
unit_shape_ = QPolygonF(row_rect);
rr1 = row_rect;
unit_tr_ = row_rect;
if (row2_bits > 0) {
row_rect = QRectF(0, 1, row2_bits, 1);
unit_shape_ = unit_shape_.united(QPolygonF(row_rect));
rr2 = row_rect;
if (row2_bits > row1_bits) {
unit_tr_ = row_rect;
}
if (row3_bits > 0) {
row_rect = QRectF(0, 2, row3_bits, 1);
unit_shape_ = unit_shape_.united(QPolygonF(row_rect));
rr3 = row_rect;
}
QPainterPath pp;
pp.addPolygon(unit_shape_);
unit_shape_ = pp.simplified().toFillPolygon();
}
updateLayout();
if (finfo_->isValid()) {
setToolTip(QString("%1 (%2) = %3")
.arg(finfo_->headerInfo().name)
.arg(finfo_->headerInfo().abbreviation)
.arg(finfo_->toString()));
setData(Qt::UserRole, VariantPointer<field_info>::asQVariant(finfo_->fieldInfo()));
} else {
setToolTip(QObject::tr("Gap in dissection"));
}
}
int collapsedLength() { return collapsed_len_; }
void setPos(qreal x, qreal y) {
QGraphicsPolygonItem::setPos(x, y);
updateLayout();
}
int maxLeftY() {
qreal rel_len = (start_bit_ % layout_->bitsPerRow()) + collapsed_len_;
QPointF pt = mapToParent(QPointF(0, ceil(rel_len / layout_->bitsPerRow()) * layout_->rowHeight()));
return pt.y();
}
int maxRightY() {
qreal rel_len = (start_bit_ % layout_->bitsPerRow()) + collapsed_len_;
QPointF pt = mapToParent(QPointF(0, floor(rel_len / layout_->bitsPerRow()) * layout_->rowHeight()));
return pt.y();
}
void paint(QPainter *painter, const QStyleOptionGraphicsItem *option, QWidget *) {
painter->setPen(Qt::NoPen);
painter->save();
if (!finfo_->isValid()) {
QBrush brush = QBrush(option->palette.text().color(), Qt::BDiagPattern);
painter->setBrush(brush);
} else if (isSelected()) {
painter->setBrush(option->palette.highlight().color());
}
painter->drawPolygon(polygon());
painter->restore();
// Lower and inner right borders
painter->setPen(option->palette.text().color());
QPolygonF shape = polygon();
for (int idx = 1; idx < unit_shape_.size(); idx++) {
QPointF u_start = unit_shape_[idx - 1];
QPointF u_end = unit_shape_[idx];
QPointF start, end;
bool draw_line = false;
if (u_start.y() > 0 && u_start.y() == u_end.y()) {
draw_line = true;
} else if (u_start.x() > 0 && u_start.x() < layout_->bitsPerRow() && u_start.x() == u_end.x()) {
draw_line = true;
}
if (draw_line) {
start = shape[idx - 1];
end = shape[idx];
painter->drawLine(start, end);
}
}
if (!finfo_->isValid()) {
return;
}
// Field label(s)
QString label = finfo_->headerInfo().name;
paintLabel(painter, label, scaled_tr_);
if (layout_->showFields()) {
label = finfo_->toString();
paintLabel(painter, label, scaled_tr_.adjusted(0, scaled_tr_.height(), 0, scaled_tr_.height()));
}
}
private:
enum SpanMark {
TopLeft,
BottomLeft,
TopRight,
BottomRight,
NumSpanMarks
};
FieldInformation *finfo_;
int start_bit_;
const DiagramLayout *layout_;
int collapsed_len_;
int collapsed_row_;
QPolygonF unit_shape_;
QRectF unit_tr_;
QRectF scaled_tr_;
QGraphicsLineItem *span_marks_[NumSpanMarks];
void updateLayout() {
QTransform xform;
xform.scale(layout_->bitWidth(), layout_->rowHeight());
setPolygon(xform.map(unit_shape_));
scaled_tr_ = xform.mapRect(unit_tr_);
scaled_tr_.adjust(layout_->hPadding(), layout_->vPadding(), -layout_->hPadding(), -layout_->vPadding());
scaled_tr_.setHeight(layout_->lineHeight());
// Collapsed / span marks
for (int idx = 0; idx < NumSpanMarks; idx++) {
span_marks_[idx]->hide();
}
if (collapsed_row_ >= 0) {
QRectF bounding_rect = polygon().boundingRect();
qreal center_y = bounding_rect.top() + (layout_->rowHeight() * collapsed_row_) + (layout_->rowHeight() / 2);
qreal mark_w = layout_->bitWidth() / 3; // Each mark side to center
QLineF span_l = QLineF(-mark_w, mark_w / 2, mark_w, -mark_w / 2);
for (int idx = 0; idx < NumSpanMarks; idx++) {
QPointF center;
switch (idx) {
case TopLeft:
center = QPointF(bounding_rect.left(), center_y - layout_->spanMarkOffset());
break;
case BottomLeft:
center = QPointF(bounding_rect.left(), center_y + layout_->spanMarkOffset());
break;
case TopRight:
center = QPointF(bounding_rect.right(), center_y - layout_->spanMarkOffset());
break;
case BottomRight:
center = QPointF(bounding_rect.right(), center_y + layout_->spanMarkOffset());
break;
}
span_marks_[idx]->setLine(span_l.translated(center));
span_marks_[idx]->setZValue(zValue() - 0.1);
span_marks_[idx]->show();
}
}
}
void paintLabel(QPainter *painter, QString label, QRectF label_rect) {
QFontMetrics fm = QFontMetrics(layout_->regularFont());
painter->setFont(layout_->regularFont());
#if (QT_VERSION >= QT_VERSION_CHECK(5, 11, 0))
int label_w = fm.horizontalAdvance(label);
#else
int label_w = fm.width(label);
#endif
if (label_w > label_rect.width()) {
painter->setFont(layout_->smallFont());
fm = QFontMetrics(layout_->smallFont());
#if (QT_VERSION >= QT_VERSION_CHECK(5, 11, 0))
label_w = fm.horizontalAdvance(label);
#else
label_w = fm.width(label);
#endif
if (label_w > label_rect.width()) {
// XXX Use parent+ItemClipsChildrenToShape or setScale instead?
label = fm.elidedText(label, Qt::ElideRight, label_rect.width());
}
}
painter->drawText(label_rect, Qt::AlignCenter, label);
}
};
PacketDiagram::PacketDiagram(QWidget *parent) :
QGraphicsView(new QGraphicsScene(), parent),
layout_(new DiagramLayout),
cap_file_(nullptr),
root_node_(nullptr),
selected_field_(nullptr),
y_pos_(0)
{
setAccessibleName(tr("Packet diagram"));
setRenderHint(QPainter::Antialiasing);
// XXX Move to setMonospaceFont similar to ProtoTree
layout_->setFont(font());
connect(wsApp, &WiresharkApplication::appInitialized, this, &PacketDiagram::connectToMainWindow);
QGraphicsScene *this_scene = scene();
connect(this_scene, &QGraphicsScene::selectionChanged, this, &PacketDiagram::sceneSelectionChanged);
connect(wsApp, &WiresharkApplication::zoomRegularFont, this, &PacketDiagram::setFont);
}
PacketDiagram::~PacketDiagram()
{
delete layout_;
}
void PacketDiagram::setRootNode(proto_node *root_node)
{
scene()->clear();
root_node_ = root_node;
selected_field_ = nullptr;
y_pos_ = 0;
ProtoNode parent_node(root_node_);
if (!parent_node.isValid()) {
return;
}
ProtoNode::ChildIterator kids = parent_node.children();
while (kids.element().isValid())
{
proto_node *tl_node = kids.element().protoNode();
kids.next();
// Exclude all ("Frame") and nothing
if (tl_node->finfo->start == 0 && tl_node->finfo->length == (int) tvb_captured_length(cap_file_->edt->tvb)) {
continue;
}
if (tl_node->finfo->length < 1) {
continue;
}
addDiagram(tl_node);
}
}
void PacketDiagram::clear()
{
setRootNode(nullptr);
}
void PacketDiagram::setCaptureFile(capture_file *cf)
{
// For use by the main view, set the capture file which will later have a
// dissection (EDT) ready.
// The packet dialog sets a fixed EDT context and MUST NOT use this.
cap_file_ = cf;
}
void PacketDiagram::setFont(const QFont &font)
{
layout_->setFont(font);
setRootNode(root_node_);
}
void PacketDiagram::selectedFieldChanged(FieldInformation *finfo)
{
setSelectedField(finfo ? finfo->fieldInfo() : nullptr);
}
void PacketDiagram::selectedFrameChanged(QList<int> frames)
{
if (frames.count() == 1 && cap_file_ && cap_file_->edt && cap_file_->edt->tree) {
setRootNode(cap_file_->edt->tree);
} else {
// Clear the proto tree contents as they have become invalid.
setRootNode(nullptr);
}
}
void PacketDiagram::contextMenuEvent(QContextMenuEvent *event)
{
if (!event) {
return;
}
QAction *action;
QMenu ctx_menu(this);
action = ctx_menu.addAction(tr("Show Field Values"));
action->setCheckable(true);
action->setChecked(layout_->showFields());
connect(action, &QAction::toggled, this, &PacketDiagram::showFieldsToggled);
ctx_menu.addSeparator();
action = ctx_menu.addAction(tr("Save Diagram As" UTF8_HORIZONTAL_ELLIPSIS));
connect(action, &QAction::triggered, this, &PacketDiagram::saveAsTriggered);
action = ctx_menu.addAction(tr("Copy as Raster Image"));
connect(action, &QAction::triggered, this, &PacketDiagram::copyAsRasterTriggered);
#if defined(QT_SVG_LIB) && !defined(Q_OS_MAC)
action = ctx_menu.addAction(tr(UTF8_HORIZONTAL_ELLIPSIS "as SVG"));
connect(action, &QAction::triggered, this, &PacketDiagram::copyAsSvgTriggered);
#endif
ctx_menu.exec(event->globalPos());
}
void PacketDiagram::connectToMainWindow()
{
MainWindow *main_window = qobject_cast<MainWindow *>(wsApp->mainWindow());
if (!main_window) {
return;
}
connect(main_window, &MainWindow::setCaptureFile, this, &PacketDiagram::setCaptureFile);
connect(main_window, &MainWindow::fieldSelected, this, &PacketDiagram::selectedFieldChanged);
connect(main_window, &MainWindow::framesSelected, this, &PacketDiagram::selectedFrameChanged);
connect(this, &PacketDiagram::fieldSelected, main_window, &MainWindow::fieldSelected);
}
void PacketDiagram::sceneSelectionChanged()
{
field_info *sel_fi = nullptr;
if (! scene()->selectedItems().isEmpty()) {
sel_fi = VariantPointer<field_info>::asPtr(scene()->selectedItems().first()->data(Qt::UserRole));
}
if (sel_fi) {
FieldInformation finfo(sel_fi, this);
emit fieldSelected(&finfo);
} else {
emit fieldSelected(nullptr);
}
}
struct WireItem {
proto_item *item;
int start_bit;
int length;
};
void PacketDiagram::addDiagram(proto_node *tl_node)
{
QGraphicsItem *item;
QGraphicsSimpleTextItem *t_item;
int bits_per_row = layout_->bitsPerRow();
int bit_width = layout_->bitWidth();
int diag_w = bit_width * layout_->bitsPerRow();
qreal x = layout_->hPadding();
// Title
t_item = scene()->addSimpleText(tl_node->finfo->hfinfo->name);
t_item->setFont(layout_->regularFont());
t_item->setPos(0, y_pos_);
y_pos_ += layout_->lineHeight() + (bit_width / 4);
int border_top = y_pos_;
// Bit scale + tick marks
QList<int> tick_nums;
for (int tn = 0 ; tn < layout_->bitsPerRow(); tn += 16) {
tick_nums << tn << tn + 15;
}
qreal y_bottom = y_pos_ + bit_width;
QGraphicsItem *tl_item = scene()->addLine(x, y_bottom, x + diag_w, y_bottom);
for (int tick_n = 0; tick_n < bits_per_row; tick_n++) {
x = layout_->hPadding() + (tick_n * bit_width);
qreal y_top = y_pos_ + (tick_n % 8 == 0 ? 0 : bit_width / 2);
if (tick_n > 0) {
scene()->addLine(x, y_top, x, y_bottom);
}
if (tick_nums.contains(tick_n)) {
t_item = scene()->addSimpleText(QString::number(tick_n));
t_item->setFont(layout_->smallFont());
t_item->setPos(x + ((bit_width - t_item->boundingRect().width()) / 2.0) + 2, y_pos_);
}
}
y_pos_ = y_bottom;
x = layout_->hPadding();
// Top-level fields
int last_start_bit = -1;
int max_l_y = y_bottom;
QList<WireItem>wire_items;
for (proto_item *cur_item = tl_node->first_child; cur_item; cur_item = cur_item->next) {
if (proto_item_is_generated(cur_item) || proto_item_is_hidden(cur_item)) {
continue;
}
field_info *fi = cur_item->finfo;
int start_bit = ((fi->start - tl_node->finfo->start) * 8) + FI_GET_BITS_OFFSET(fi);
int length = FI_GET_BITS_SIZE(fi) ? FI_GET_BITS_SIZE(fi) : fi->length * 8;
if (start_bit <= last_start_bit || length <= 0) {
qDebug() << "Skipping pass 1" << fi->hfinfo->abbrev << start_bit << last_start_bit << length;
continue;
}
last_start_bit = start_bit;
WireItem wire_item = { cur_item, start_bit, length };
wire_items << wire_item;
}
qreal z_value = tl_item->zValue();
for (int idx = 0; idx < wire_items.size(); idx++) {
WireItem *wire_item = &wire_items[idx];
field_info *fi = wire_item->item->finfo;
if (idx < wire_items.size() - 1) {
WireItem *next_item = &wire_items[idx + 1];
if (wire_item->start_bit + wire_item->length > next_item->start_bit) {
wire_item->length = next_item->start_bit - wire_item->start_bit;
qDebug() << "Resized pass 2" << fi->hfinfo->abbrev << wire_item->start_bit << wire_item->length << next_item->start_bit;
if (wire_item->length <= 0) {
qDebug() << "Skipping pass 2" << fi->hfinfo->abbrev << wire_item->start_bit << wire_item->length;
continue;
}
}
if (next_item->start_bit > wire_item->start_bit + wire_item->length) {
int gap_start_bit = wire_item->start_bit + wire_item->length;
int gap_len = next_item->start_bit - gap_start_bit;
int y_off = (gap_start_bit / bits_per_row) * layout_->rowHeight();
// Stack each item behind the previous one.
z_value -= .01;
FieldInformationGraphicsItem *gap_item = new FieldInformationGraphicsItem(nullptr, gap_start_bit, gap_len, layout_);
gap_item->setPos(x, y_bottom + y_off);
gap_item->setZValue(z_value);
scene()->addItem(gap_item);
}
}
int y_off = (wire_item->start_bit / bits_per_row) * layout_->rowHeight();
// Stack each item behind the previous one.
z_value -= .01;
FieldInformationGraphicsItem *fi_item = new FieldInformationGraphicsItem(fi, wire_item->start_bit, wire_item->length, layout_);
fi_item->setPos(x, y_bottom + y_off);
fi_item->setFlag(QGraphicsItem::ItemIsSelectable);
fi_item->setAcceptedMouseButtons(Qt::LeftButton);
fi_item->setZValue(z_value);
scene()->addItem(fi_item);
y_pos_ = fi_item->maxRightY();
max_l_y = fi_item->maxLeftY();
}
// Left & right borders
scene()->addLine(x, border_top, x, max_l_y);
scene()->addLine(x + diag_w, border_top, x + diag_w, y_pos_);
// Inter-diagram margin
y_pos_ = max_l_y + bit_width;
// Set the proper color. Needed for dark mode on macOS + Qt 5.15.0 at least, possibly other cases.
foreach (item, scene()->items()) {
QGraphicsSimpleTextItem *t_item = qgraphicsitem_cast<QGraphicsSimpleTextItem *>(item);
if (t_item) {
t_item->setBrush(palette().text().color());
}
QGraphicsLineItem *l_item = qgraphicsitem_cast<QGraphicsLineItem *>(item);
if (l_item) {
l_item->setPen(palette().text().color());
}
}
}
void PacketDiagram::setSelectedField(field_info *fi)
{
QSignalBlocker blocker(this);
FieldInformationGraphicsItem *fi_item;
foreach (QGraphicsItem *item, scene()->items()) {
if (item->isSelected()) {
item->setSelected(false);
fi_item = qgraphicsitem_cast<FieldInformationGraphicsItem *>(item);
}
if (fi && VariantPointer<field_info>::asPtr(item->data(Qt::UserRole)) == fi) {
fi_item = qgraphicsitem_cast<FieldInformationGraphicsItem *>(item);
if (fi_item) {
fi_item->setSelected(true);
}
}
}
}
QImage PacketDiagram::exportToImage()
{
// Create a hi-res 2x scaled image.
int scale = 2;
QRect rr = QRect(0, 0, sceneRect().size().width() * scale, sceneRect().size().height() * scale);
QImage raster_diagram = QImage(rr.size(), QImage::Format_ARGB32);
QPainter raster_painter(&raster_diagram);
raster_painter.setRenderHint(QPainter::Antialiasing);
raster_painter.fillRect(rr, palette().base().color());
scene()->render(&raster_painter);
raster_painter.end();
return raster_diagram;
}
#if defined(QT_SVG_LIB) && 0
QByteArray PacketDiagram::exportToSvg()
{
QRect sr = QRect(0, 0, sceneRect().size().width(), sceneRect().size().height());
QBuffer svg_buf;
QSvgGenerator svg_diagram;
svg_diagram.setSize(sr.size());
svg_diagram.setViewBox(sr);
svg_diagram.setOutputDevice(&svg_buf);
QPainter svg_painter(&svg_diagram);
svg_painter.fillRect(sr, palette().base().color());
scene()->render(&svg_painter);
svg_painter.end();
return svg_buf.buffer();
}
#endif
void PacketDiagram::showFieldsToggled(bool checked)
{
layout_->setShowFields(checked);
setRootNode(root_node_);
}
// XXX - We have similar code in tcp_stream_dialog and io_graph_dialog. Should this be a common routine?
void PacketDiagram::saveAsTriggered()
{
QString file_name, extension;
QDir path(wsApp->lastOpenDir());
QString png_filter = tr("Portable Network Graphics (*.png)");
QString bmp_filter = tr("Windows Bitmap (*.bmp)");
// Gaze upon my beautiful graph with lossy artifacts!
QString jpeg_filter = tr("JPEG File Interchange Format (*.jpeg *.jpg)");
QStringList fl = QStringList() << png_filter << bmp_filter << jpeg_filter;
#if defined(QT_SVG_LIB) && 0
QString svg_filter = tr("Scalable Vector Graphics (*.svg)");
fl << svg_filter;
#endif
QString filter = fl.join(";;");
file_name = WiresharkFileDialog::getSaveFileName(this, wsApp->windowTitleString(tr("Save Graph As" UTF8_HORIZONTAL_ELLIPSIS)),
path.canonicalPath(), filter, &extension);
if (file_name.length() > 0) {
bool save_ok = false;
if (extension.compare(png_filter) == 0) {
QImage raster_diagram = exportToImage();
save_ok = raster_diagram.save(file_name, "PNG");
} else if (extension.compare(bmp_filter) == 0) {
QImage raster_diagram = exportToImage();
save_ok = raster_diagram.save(file_name, "BMP");
} else if (extension.compare(jpeg_filter) == 0) {
QImage raster_diagram = exportToImage();
save_ok = raster_diagram.save(file_name, "JPG");
}
#if defined(QT_SVG_LIB) && 0
else if (extension.compare(svg_filter) == 0) {
QByteArray svg_diagram = exportToSvg();
QFile file(file_name);
if (file.open(QIODevice::WriteOnly)) {
save_ok = file.write(svg_diagram) > 0;
file.close();
}
}
#endif
// else error dialog?
if (save_ok) {
path = QDir(file_name);
wsApp->setLastOpenDir(path.canonicalPath().toUtf8().constData());
}
}
}
void PacketDiagram::copyAsRasterTriggered()
{
QImage raster_diagram = exportToImage();
wsApp->clipboard()->setImage(raster_diagram);
}
#if defined(QT_SVG_LIB) && !defined(Q_OS_MAC) && 0
void PacketDiagram::copyAsSvgTriggered()
{
QByteArray svg_ba = exportToSvg();
// XXX It looks like we have to use/subclass QMacPasteboardMime in
// order for this to work on macOS.
// It might be easier to just do "Save As" instead.
QMimeData *md = new QMimeData();
md->setData("image/svg+xml", svg_buf);
wsApp->clipboard()->setMimeData(md);
}
#endif