For frequent Markdown users, image storage has always been a challenge. Markdown itself does not support embedded images, traditionally requiring external links to local or online files. However, publishing articles online raises storage issues. While image hosting services exist, the process can be cumbersome.

ortunately, Markdown supports image rendering via Base64 encoding. This tool Image2Base64 simplifies conversions between image files and Base64 encoding.

1. Features

Below is the graphical interface of the software (this screenshot uses Base64 rendering):

4cdbc8106d0296e6261e6abdfa0b0096

The left panel displays images. You can drag and drop images to open them, supporting formats like BMP, PNG, and JPG (GIF animations are not supported). Right-click menus provide basic operations like zooming and resetting.

The Paste button below the left panel reads image data from the clipboard, enabling seamless integration with screenshot tools.

The right panel displays the generated Base64 encoding. The “Markdown” checkbox adds Markdown image syntax tags. Clicking Copy copies the text data to the clipboard for direct pasting into MD files.

Note: The dropdown menu next to Copy allows selecting the image format for Base64 encoding. Base64 length varies depending on the original image format, complexity, and compression.

The Save as button below the right panel saves images in their original format or as Base64-encoded TXT files. The bidirectional arrows in the middle convert images to Base64 or vice versa. Important: When converting Base64 to images, ensure the input contains no Markdown syntax tags to avoid errors.

Tested with PNG format, this tool successfully renders images in Marktext and Joplin . Other formats depend on the Markdown editor’s rendering engine. For screenshots, PNG is recommended.

2. Code

2.1 C++ Implementation

The software is based on Qt6. Image format encoding, rendering and base64 conversion are all implemented through Qt.

Image display is implemented through QLabel, which overloads the QLabel class1 and makes some adjustments.

photolabel.h

#ifndef PHOTOLABEL_H
#define PHOTOLABEL_H

#include <QObject>
#include <QLabel>
#include <QMenu>
#include <QDragEnterEvent>
#include <QDropEvent>

class PhotoLabel : public QLabel
{
	Q_OBJECT

public:
	explicit PhotoLabel(QWidget* parent = nullptr);

	void openFile(QString);     //打开图片文件
	void clearShow();           //清空显示
	void setImage(QImage&);     //设置图片
	void openAction();          //调用打开文件对话框
	void pasteAction();         //粘贴来自剪贴板的图片
	const QImage& getImage();   //调用存储的图片数据

signals:
	// 图片加载成功信号
	void imageLoadSuccess();

protected:
	void contextMenuEvent(QContextMenuEvent* event) override;   //右键菜单
	void paintEvent(QPaintEvent* event) override;               //QPaint画图
	void wheelEvent(QWheelEvent* event) override;               //鼠标滚轮滚动
	void mousePressEvent(QMouseEvent* event) override;          //鼠标摁下
	void mouseMoveEvent(QMouseEvent* event) override;           //鼠标松开
	void mouseReleaseEvent(QMouseEvent* event) override;        //鼠标发射事件
	//拖放文件
	void dragEnterEvent(QDragEnterEvent* event) override;
	void dragMoveEvent(QDragMoveEvent* event) override;
	void dropEvent(QDropEvent* event) override;

private slots:
	void initWidget();      //初始化
	void onSelectImage();   //选择打开图片
	void onPasteImage();    //选择粘贴图片
	void onZoomInImage();   //图片放大
	void onZoomOutImage();  //图片缩小
	void onPresetImage();   //图片还原

private:
	QImage m_image;           //显示的图片
	qreal m_zoomValue = 1.0;  //鼠标缩放值
	int m_xPtInterval = 0;    //平移X轴的值
	int m_yPtInterval = 0;    //平移Y轴的值
	QPoint m_oldPos;          //旧的鼠标位置
	bool m_pressed = false;   //鼠标是否被摁压
	QString m_localFileName;  //文件名称
	QMenu* m_menu;            //右键菜单
};

#endif // PHOTOLABEL_H

photolabel.cpp

#include "photolabel.h"
#include <QPainter>
#include <QDebug>
#include <QWheelEvent>
#include <QFileDialog>
#include <QClipboard>
#include <QApplication>
#include <QMimeData>
#include <QFileInfo>
#include <QMessageBox>

PhotoLabel::PhotoLabel(QWidget* parent) :QLabel(parent)
{
	initWidget();
}

void PhotoLabel::initWidget()
{
	//初始化右键菜单
	m_menu = new QMenu(this);
	QAction* loadImage = new QAction;
	loadImage->setText(tr("&Open new..."));
	connect(loadImage, &QAction::triggered, this, &PhotoLabel::onSelectImage);
	m_menu->addAction(loadImage);

	QAction* pasteImage = new QAction;
	pasteImage->setText(tr("&Paste image"));
	connect(pasteImage, &QAction::triggered, this, &PhotoLabel::onPasteImage);
	m_menu->addAction(pasteImage);
	m_menu->addSeparator();

	QAction* zoomInAction = new QAction;
    zoomInAction->setText(tr("Zoom in &+"));
	connect(zoomInAction, &QAction::triggered, this, &PhotoLabel::onZoomInImage);
	m_menu->addAction(zoomInAction);

	QAction* zoomOutAction = new QAction;
    zoomOutAction->setText(tr("Zoom out &-"));
	connect(zoomOutAction, &QAction::triggered, this, &PhotoLabel::onZoomOutImage);
	m_menu->addAction(zoomOutAction);

	QAction* presetAction = new QAction;
	presetAction->setText(tr("&Reset"));
	connect(presetAction, &QAction::triggered, this, &PhotoLabel::onPresetImage);
	m_menu->addAction(presetAction);
	m_menu->addSeparator();
	/*
	QAction *clearAction = new QAction;
	clearAction->setText("Clear");
	connect(clearAction, &QAction::triggered, this, &PhotoLabel::clearShow);
	m_menu->addAction(clearAction);
	*/
}

void PhotoLabel::openFile(QString path)
{
	if (path.isEmpty())
	{
		return;
	}

	if (!m_image.load(path))
	{
		QMessageBox::warning(this, tr("Error"), tr("Cannot load file!"));
		return;
	}

	m_zoomValue = 1.0;
	m_xPtInterval = 0;
	m_yPtInterval = 0;

	m_localFileName = path;
	emit this->imageLoadSuccess();
	update();
}

void PhotoLabel::clearShow()
{
	m_localFileName = "";
	m_image = QImage();
	this->clear();
}

void PhotoLabel::setImage(QImage& img)
{
	if (img.isNull())
	{
		return;
	}

	m_zoomValue = 1.0;
	m_xPtInterval = 0;
	m_yPtInterval = 0;

	m_localFileName = "";
	m_image = img.copy(0, 0, img.width(), img.height());
	emit imageLoadSuccess();
	update();
}

void PhotoLabel::openAction()
{
	this->onSelectImage();
}

void PhotoLabel::pasteAction()
{
	this->onPasteImage();
}

const QImage& PhotoLabel::getImage()
{
	return m_image;
}

void PhotoLabel::paintEvent(QPaintEvent* event)
{
	if (m_image.isNull())
		return QWidget::paintEvent(event);

	QPainter painter(this);

	// 根据窗口计算应该显示的图片的大小
	int width = qMin(m_image.width(), this->width());
	int height = int(width * 1.0 / (m_image.width() * 1.0 / m_image.height()));
	height = qMin(height, this->height());
	width = int(height * 1.0 * (m_image.width() * 1.0 / m_image.height()));

	// 平移
	painter.translate(this->width() / 2 + m_xPtInterval, this->height() / 2 + m_yPtInterval);

	// 缩放
	painter.scale(m_zoomValue, m_zoomValue);

	// 绘制图像
	QRect picRect(-width / 2, -height / 2, width, height);
	painter.drawImage(picRect, m_image);

	QWidget::paintEvent(event);
}

void PhotoLabel::wheelEvent(QWheelEvent* event)
{
	int value = event->angleDelta().y() / 15;
	if (value < 0)  //放大
		onZoomInImage();
	else            //缩小
		onZoomOutImage();

	update();
}

void PhotoLabel::mousePressEvent(QMouseEvent* event)
{
	m_oldPos = event->pos();
	m_pressed = true;
	this->setCursor(Qt::ClosedHandCursor); //设置鼠标样式
}

void PhotoLabel::mouseMoveEvent(QMouseEvent* event)
{
	if (!m_pressed)
		return QWidget::mouseMoveEvent(event);

	QPoint pos = event->pos();
	int xPtInterval = pos.x() - m_oldPos.x();
	int yPtInterval = pos.y() - m_oldPos.y();

	m_xPtInterval += xPtInterval;
	m_yPtInterval += yPtInterval;

	m_oldPos = pos;
	update();
}

void PhotoLabel::mouseReleaseEvent(QMouseEvent*/*event*/)
{
	m_pressed = false;
	this->setCursor(Qt::ArrowCursor); //设置鼠标样式
}

void PhotoLabel::dragEnterEvent(QDragEnterEvent* event)
{
	if (event->mimeData()->hasUrls())
	{
		event->acceptProposedAction();
	}
	else
	{
		event->ignore();
	}
}

void PhotoLabel::dragMoveEvent(QDragMoveEvent* event)
{

}

void PhotoLabel::dropEvent(QDropEvent* event)
{
	QList<QUrl> urls = event->mimeData()->urls();
	if (urls.empty())
		return;

	QString fileName = urls.first().toLocalFile();
	QFileInfo info(fileName);
	if (!info.isFile())
		return;

	this->openFile(fileName);
}

void PhotoLabel::contextMenuEvent(QContextMenuEvent* event)
{
	QPoint pos = event->pos();
	pos = this->mapToGlobal(pos);
	m_menu->exec(pos);
}

void PhotoLabel::onSelectImage()
{
	QString path = QFileDialog::getOpenFileName(this, tr("Open a image file"), "./", tr("Images (*.bmp *.png *.jpg *.jpeg *.gif *.tiff)\nAll files (*.*)"));
	if (path.isEmpty())
		return;

	openFile(path);
}

void PhotoLabel::onPasteImage()
{
	QClipboard* clipboard = QApplication::clipboard();
	QImage img = clipboard->image();
	if (img.isNull())
	{
		return;
	}

	this->setImage(img);
}

void PhotoLabel::onZoomInImage()
{
	m_zoomValue += 0.05;
	update();
}

void PhotoLabel::onZoomOutImage()
{
	m_zoomValue -= 0.05;
	if (m_zoomValue <= 0)
	{
		m_zoomValue = 0.05;
		return;
	}

	update();
}

void PhotoLabel::onPresetImage()
{
	m_zoomValue = 1.0;
	m_xPtInterval = 0;
	m_yPtInterval = 0;
	update();
}

The base64 and image conversion code is placed in the slot function of the window control.

widget.cpp

void Widget::updateCode() //图片数据转换成base64编码
{
	QImage image = ui->viewer->getImage();
	if (image.isNull())
	{
		QMessageBox::warning(this, tr("Error"), tr("Please load an image file!"));
		return;
	}
	QByteArray ba;
	QBuffer buf(&ba);
	image.save(&buf, format.toStdString().c_str());

	QByteArray md5 = QCryptographicHash::hash(ba, QCryptographicHash::Md5);
	QString strMd5 = md5.toHex();

	QString head_md = QString::fromUtf8("![%1](%2)");
	QString prefix = QString::fromUtf8("data:image/%1;base64,").arg(format);
	QString code = QString::fromUtf8(ba.toBase64());

	if (ui->checkBox->isChecked())
	{
		ui->textEdit->setText(head_md.arg(strMd5).arg(prefix + code));
	}
	else
	{
		ui->textEdit->setText(prefix + code);
	}

	buf.close();

	int num = ui->textEdit->toPlainText().length();
	ui->label->setText(tr("Length : ") + QString::number(num));
}


void Widget::updateImage() // base64编码转换成图片数据
{
	QString p_b = ui->textEdit->toPlainText();
	if (p_b.isEmpty())
	{
		return;
	}
	if (p_b.contains(QRegularExpression("data:image[/a-z]*;base64,")))
	{
		// 清除base64编码的html标签
		p_b = p_b.remove(QRegularExpression("data:image[/a-z]*;base64,"));
	}

	QImage image;
	if (!image.loadFromData(QByteArray::fromBase64(p_b.toLocal8Bit())))
	{
		QMessageBox::warning(this, tr("Error"), tr("Fail to decrypt codes!"));
		return;
	}
	ui->viewer->setImage(image);
}

2.2 Python Implementation

Similarly, the software also provides a Python-based implementation, which also realizes image display by overloading QLabel.

photolabel.py

# This Python file uses the following encoding: utf-8
import sys

from PySide6.QtCore import (Qt, qDebug, QFileInfo, QMimeData, QRect, QPoint)

from PySide6.QtGui import (QAction, QImage, QAction, QDragEnterEvent, QDragMoveEvent,
                           QDropEvent, QContextMenuEvent, QPaintEvent, QMouseEvent,
                           QWheelEvent, QPainter, QClipboard, QCursor)

from PySide6.QtWidgets import (QApplication, QLabel, QMenu, QMessageBox, QFileDialog)


class PhotoLabel(QLabel):
    def __init__(self, parent):
        super().__init__(parent)
        self.m_image = QImage()  # 显示的图片
        self.m_zoomValue = 1.0  # 鼠标缩放值
        self.m_xPtInterval = 0  # 平移X轴的值
        self.m_yPtInterval = 0  # 平移Y轴的值
        self.m_oldPos = QPoint()  # 旧的鼠标位置
        self.m_pressed = False  # 鼠标是否被摁压
        self.m_localFileName = None  # 文件名称
        self.m_menu = None
        self.initial_widget()

    def initial_widget(self):
        self.m_menu = QMenu(self)
        load_image = QAction(u"&Open new...", self)
        load_image.triggered.connect(self.on_select_image)
        self.m_menu.addAction(load_image)
        paste_image = QAction(u"&Paste image", self)
        paste_image.triggered.connect(self.on_paste_image)
        self.m_menu.addAction(paste_image)
        self.m_menu.addSeparator()
        zoom_in_action = QAction(u"Zoom in &+", self)
        zoom_in_action.triggered.connect(self.on_zoom_in_image)
        self.m_menu.addAction(zoom_in_action)
        zoom_out_action = QAction(u"Zoom out &-", self)
        zoom_out_action.triggered.connect(self.on_zoom_out_image)
        self.m_menu.addAction(zoom_out_action)
        self.m_menu.addSeparator()
        preset_action = QAction(u"&Reset", self)
        preset_action.triggered.connect(self.on_preset_image)
        self.m_menu.addAction(preset_action)
        self.m_menu.addSeparator()
        # clear_action = QAction(u"&Reset", self)
        # clear_action.triggered.connect(self.clear_show)
        # self.m_menu.addAction(clear_action)

    def open_file(self, path: str):  # 打开图片文件
        if path is None:
            return
        if self.m_image.load(path) is False:
            QMessageBox.warning(self, "Error", "Cannot load file!")
            return
        self.m_zoomValue = 1.0
        self.m_xPtInterval = 0
        self.m_yPtInterval = 0
        self.m_localFileName = path
        self.update()

    def clear_show(self):  # 清空显示
        self.m_localFileName = None
        self.m_image = QImage()
        self.clear()

    def set_image(self, image: QImage):  # 设置图片
        if image is None:
            return
        self.m_zoomValue = 1.0
        self.m_xPtInterval = 0
        self.m_yPtInterval = 0
        self.m_localFileName = None
        self.m_image = image.copy(0, 0, image.width(), image.height())
        self.update()

    def open_action(self):  # 调用打开文件对话框
        self.on_select_image()

    def paste_action(self):  # 粘贴来自剪贴板的图片
        self.on_paste_image()

    def get_image(self) -> QImage:  # 调用存储的图片数据
        return self.m_image

    def contextMenuEvent(self, event: QContextMenuEvent):  # 右键菜单
        pos = event.pos()
        pos = self.mapToGlobal(pos)
        self.m_menu.exec(pos)

    def paintEvent(self, event: QPaintEvent):  # QPaint画图
        if self.m_image.isNull():
            # super().paintEvent(event)
            return
        painter = QPainter(self)
        # 根据窗口计算应该显示的图片的大小
        width = min(self.m_image.width(), self.width())
        height = int(width * 1.0 / (self.m_image.width() * 1.0 / self.m_image.height()))
        height = min(height, self.height())
        width = int(height * 1.0 * (self.m_image.width() * 1.0 / self.m_image.height()))
        # 平移
        painter.translate(self.width() / 2 + self.m_xPtInterval, self.height() / 2 + self.m_yPtInterval)
        # 缩放
        painter.scale(self.m_zoomValue, self.m_zoomValue)
        # 绘制图像
        pic_rect = QRect(int(-width / 2), int(-height / 2), width, height)
        painter.drawImage(pic_rect, self.m_image)
        # super().paintEvent(event)

    def wheelEvent(self, event: QWheelEvent):  # 鼠标滚轮滚动
        value = int(event.angleDelta().y() / 15)
        if value < 0:  # 放大
            self.on_zoom_in_image()
        else:  # 缩小
            self.on_zoom_out_image()
        self.update()

    def mousePressEvent(self, event: QMouseEvent):  # 鼠标摁下
        self.m_oldPos = event.pos()
        self.m_pressed = True
        self.setCursor(Qt.ClosedHandCursor)  # 设置鼠标样式

    def mouseMoveEvent(self, event: QMouseEvent):  # 鼠标松开
        if self.m_pressed is False:
            # super().mouseMoveEvent(event)
            return
        pos = event.pos()
        xp = pos.x() - self.m_oldPos.x()
        yp = pos.y() - self.m_oldPos.y()
        self.m_xPtInterval += xp
        self.m_yPtInterval += yp
        self.m_oldPos = pos
        self.update()

    def mouseReleaseEvent(self, event: QMouseEvent):  # 鼠标发射事件
        self.m_pressed = False
        self.setCursor(Qt.ArrowCursor)  # 设置鼠标样式

    # 拖放文件
    def dragEnterEvent(self, event: QDragEnterEvent):
        if event.mimeData().hasUrls():
            event.acceptProposedAction()
        else:
            event.ignore()

    def dragMoveEvent(self, event: QDragMoveEvent):
        pass

    def dropEvent(self, event: QDropEvent):
        urls = event.mimeData().urls()
        if urls is None:
            return
        file_name = urls[0].toLocalFile()
        info = QFileInfo(file_name)
        if info.isFile() is False:
            return
        self.open_file(file_name)

    def on_select_image(self):  # 选择打开图片
        path, _ = QFileDialog.getOpenFileName(self,
                                              "Open a image file", "./",
                                              "Images (*.bmp *.png *.jpg *.jpeg *.gif *.tiff)\nAll files (*.*)")
        if path is None:
            return
        info = QFileInfo(path)
        if info.isFile() is False:
            return
        self.open_file(path)

    def on_paste_image(self):  # 选择粘贴图片
        clipboard = QApplication.clipboard()
        img = clipboard.image()
        if img.isNull():
            return
        self.set_image(img)

    def on_zoom_in_image(self):  # 图片放大
        self.m_zoomValue += 0.05
        self.update()

    def on_zoom_out_image(self):  # 图片缩小
        self.m_zoomValue -= 0.05
        if self.m_zoomValue <= 0:
            self.m_zoomValue = 0.05
            return
        self.update()

    def on_preset_image(self):  # 图片还原
        self.m_zoomValue = 1.0
        self.m_xPtInterval = 0
        self.m_yPtInterval = 0
        self.update()


if __name__ == "__main__":
    pass

It is worth noting that in the Python implementation code, the conversion of binary data to base64 is completed through Python’s str function, so the output string will contain the label b'...', which needs to be removed through string slicing.widget.py

def update_code(self): #图片→base64
        image = self.ui.viewLabel.get_image()
        if image.isNull():
            QMessageBox.warning(self, "Error", "Please load an image file!")
            return
        ba = QByteArray()
        buf = QBuffer(ba)
        image.save(buf, self.m_format)
        md5 = QCryptographicHash.hash(ba, QCryptographicHash.Md5)
        strMd5 = str(md5.toHex())[2:-1]
        prefix = "data:image/{};base64,".format(self.m_format)
        code = str(ba.toBase64())[2:-1]
        if self.ui.checkBox.isChecked():
            self.ui.textEdit.setText("![{0}]({1})".format(strMd5, prefix + code))
        else:
            self.ui.textEdit.setText(prefix + code)
        buf.close()
        num = len(self.ui.textEdit.toPlainText())
        self.ui.lengthLabel.setText("Length : " + str(num))

    def update_image(self): #base64→图片
        p_b = self.ui.textEdit.toPlainText()
        if len(p_b) == 0:
            return
        if re.match("data:image[/a-z]*;base64,", p_b):
            p_b = re.sub("data:image[/a-z]*;base64,", "", p_b, count=1)
        image = QImage()
        if image.loadFromData(QByteArray.fromBase64(p_b.encode())) is False:
            QMessageBox.warning(self, "Error", "Fail to decrypt codes!")
            return
        self.ui.viewLabel.set_image(image)

The UI is all implemented through QtDesigner. Whether it is implemented in C++ or Python, the software interface effect is consistent.