v1.1.2
This commit is contained in:
parent
b50ee48459
commit
cfaec7e217
|
@ -1,75 +1,17 @@
|
|||
#include "settings.h"
|
||||
#include <DMessageManager>
|
||||
#include <DSettingsOption>
|
||||
#include <QApplication>
|
||||
#include <QSettings>
|
||||
#include <QStandardPaths>
|
||||
#include <QStyleFactory>
|
||||
|
||||
Settings *Settings::s_pSetting = nullptr;
|
||||
|
||||
Settings::Settings(QObject *parent) : QObject(parent) {
|
||||
QString strConfigPath =
|
||||
QString("%1/%2/%3/config.conf")
|
||||
.arg(QStandardPaths::writableLocation(QStandardPaths::ConfigLocation))
|
||||
.arg(qApp->organizationName())
|
||||
.arg(qApp->applicationName());
|
||||
Settings::Settings(QObject *parent) : QObject(parent) {}
|
||||
|
||||
m_backend = new QSettingBackend(strConfigPath);
|
||||
|
||||
settings = DSettings::fromJsonFile(":/settings.json");
|
||||
settings->setBackend(m_backend);
|
||||
|
||||
#define BindConfigSignal(Var, SettingName, Signal) \
|
||||
auto Var = settings->option(SettingName); \
|
||||
connect(Var, &Dtk::Core::DSettingsOption::valueChanged, \
|
||||
this, [=](QVariant value) Signal);
|
||||
|
||||
BindConfigSignal(quality, "", { emit sigAdjustQuality(value.toInt()); })
|
||||
|
||||
auto windowState = settings->option("appearance.window.windowsize");
|
||||
QMap<QString, QVariant> windowStateMap;
|
||||
windowStateMap.insert("keys", QStringList() << "window_normal"
|
||||
<< "window_maximum"
|
||||
<< "window_minimum"
|
||||
<< "fullscreen");
|
||||
windowStateMap.insert("values", QStringList()
|
||||
<< tr("Normal") << tr("Maximum")
|
||||
<< tr("Minimum") << tr("Fullscreen"));
|
||||
windowState->setData("items", windowStateMap);
|
||||
}
|
||||
|
||||
Settings::~Settings() {}
|
||||
|
||||
void Settings::setSettingDialog(DSettingsDialog *settingsDialog) {
|
||||
m_pSettingsDialog = settingsDialog;
|
||||
}
|
||||
|
||||
void Settings::applySetting() {
|
||||
|
||||
#define Apply(Var, SettingName, Signal) \
|
||||
auto Var = settings->option(SettingName); \
|
||||
if (Var != nullptr) \
|
||||
emit Signal;
|
||||
|
||||
Apply(quality, "editor.image.quality",
|
||||
sigAdjustQuality(quality->value().toInt()));
|
||||
Apply(windowstate, "appearance.window.windowsize",
|
||||
sigChangeWindowState(windowstate->value().toString()));
|
||||
}
|
||||
|
||||
// This function is workaround, it will remove after DTK fixed SettingDialog
|
||||
// theme bug.
|
||||
void Settings::dtkThemeWorkaround(QWidget *parent, const QString &theme) {
|
||||
parent->setStyle(QStyleFactory::create(theme));
|
||||
|
||||
for (auto obj : parent->children()) {
|
||||
auto w = qobject_cast<QWidget *>(obj);
|
||||
if (!w) {
|
||||
continue;
|
||||
}
|
||||
|
||||
dtkThemeWorkaround(w, theme);
|
||||
void Settings::saveWindowStatus(DMainWindow *wnd) {
|
||||
if (wnd != nullptr) {
|
||||
QSettings settings(QApplication::organizationName(),
|
||||
QApplication::applicationName());
|
||||
settings.setValue("geometry", wnd->saveGeometry());
|
||||
settings.setValue("windowState", wnd->saveState());
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -80,32 +22,7 @@ Settings *Settings::instance() {
|
|||
return s_pSetting;
|
||||
}
|
||||
|
||||
DDialog *Settings::createDialog(const QString &title, const QString &content,
|
||||
const bool &bIsConflicts) {
|
||||
DDialog *dialog = new DDialog(title, content, m_pSettingsDialog);
|
||||
dialog->setWindowFlags(dialog->windowFlags() | Qt::WindowStaysOnBottomHint);
|
||||
dialog->setIcon(QIcon(":/images/setting.png"));
|
||||
|
||||
if (bIsConflicts) {
|
||||
dialog->addButton(QString(tr("Cancel")), true, DDialog::ButtonNormal);
|
||||
dialog->addButton(QString(tr("Replace")), false, DDialog::ButtonRecommend);
|
||||
} else {
|
||||
dialog->addButton(QString(tr("OK")), true, DDialog::ButtonRecommend);
|
||||
}
|
||||
|
||||
return dialog;
|
||||
}
|
||||
|
||||
void Settings::saveWindowState(DMainWindow *wnd) {
|
||||
if (wnd != nullptr) {
|
||||
QSettings settings(QApplication::organizationName(),
|
||||
QApplication::applicationName());
|
||||
settings.setValue("geometry", wnd->saveGeometry());
|
||||
settings.setValue("windowState", wnd->saveState());
|
||||
}
|
||||
}
|
||||
|
||||
void Settings::loadWindowState(DMainWindow *wnd) {
|
||||
void Settings::loadWindowStatus(DMainWindow *wnd) {
|
||||
if (wnd != nullptr) {
|
||||
QSettings settings(QApplication::organizationName(),
|
||||
QApplication::applicationName());
|
||||
|
@ -114,12 +31,48 @@ void Settings::loadWindowState(DMainWindow *wnd) {
|
|||
}
|
||||
}
|
||||
|
||||
void Settings::setWindowState(int state) {
|
||||
QSettings settings(QApplication::organizationName(),
|
||||
QApplication::applicationName());
|
||||
settings.setValue("state", state);
|
||||
}
|
||||
|
||||
int Settings::windowState() {
|
||||
QSettings settings(QApplication::organizationName(),
|
||||
QApplication::applicationName());
|
||||
return settings.value("state", 0).toInt();
|
||||
}
|
||||
|
||||
void Settings::saveFileDialogCurrent(QString path) {
|
||||
QSettings settings(QApplication::organizationName(),
|
||||
QApplication::applicationName());
|
||||
settings.setValue("curpath", path);
|
||||
}
|
||||
|
||||
void Settings::setTerminal(QString path) {
|
||||
QSettings settings(QApplication::organizationName(),
|
||||
QApplication::applicationName());
|
||||
settings.setValue("terminal", path);
|
||||
}
|
||||
|
||||
QString Settings::terminal() {
|
||||
QSettings settings(QApplication::organizationName(),
|
||||
QApplication::applicationName());
|
||||
return settings.value("terminal", "/bin/bash").toString().trimmed();
|
||||
}
|
||||
|
||||
void Settings::setReprocessingCmdline(QString cmdline) {
|
||||
QSettings settings(QApplication::organizationName(),
|
||||
QApplication::applicationName());
|
||||
settings.setValue("cmdline", cmdline);
|
||||
}
|
||||
|
||||
QString Settings::reprocessingCmdline() {
|
||||
QSettings settings(QApplication::organizationName(),
|
||||
QApplication::applicationName());
|
||||
return settings.value("cmdline").toString().trimmed();
|
||||
}
|
||||
|
||||
QString Settings::loadFileDialogCurrent() {
|
||||
QSettings settings(QApplication::organizationName(),
|
||||
QApplication::applicationName());
|
||||
|
|
|
@ -1,48 +1,39 @@
|
|||
#ifndef SETTINGS_H
|
||||
#define SETTINGS_H
|
||||
|
||||
#include "GifImage/encoder/GifEncoder.h"
|
||||
#include <DDialog>
|
||||
#include <DMainWindow>
|
||||
#include <DSettingsDialog>
|
||||
#include <QObject>
|
||||
#include <qsettingbackend.h>
|
||||
|
||||
DWIDGET_USE_NAMESPACE
|
||||
DCORE_USE_NAMESPACE
|
||||
DTK_USE_NAMESPACE
|
||||
|
||||
class Settings : public QObject {
|
||||
Q_OBJECT
|
||||
public:
|
||||
explicit Settings(QObject *parent = nullptr);
|
||||
~Settings();
|
||||
|
||||
void dtkThemeWorkaround(QWidget *parent, const QString &theme);
|
||||
static Settings *instance();
|
||||
|
||||
void setSettingDialog(DSettingsDialog *settingsDialog);
|
||||
void applySetting();
|
||||
void saveWindowState(DMainWindow *wnd);
|
||||
void loadWindowState(DMainWindow *wnd);
|
||||
void saveWindowStatus(DMainWindow *wnd);
|
||||
void loadWindowStatus(DMainWindow *wnd);
|
||||
|
||||
void setWindowState(int state);
|
||||
int windowState();
|
||||
|
||||
QString loadFileDialogCurrent();
|
||||
void saveFileDialogCurrent(QString path);
|
||||
|
||||
DSettings *settings;
|
||||
void setTerminal(QString path);
|
||||
QString terminal();
|
||||
|
||||
void setReprocessingCmdline(QString cmdline);
|
||||
QString reprocessingCmdline();
|
||||
|
||||
signals:
|
||||
void sigAdjustQuality(int quality);
|
||||
void sigChangeWindowState(QString state);
|
||||
void sigreprocessingCmdlineChanged();
|
||||
void sigFileDialogCurrentChanged();
|
||||
|
||||
private:
|
||||
DDialog *createDialog(const QString &title, const QString &content,
|
||||
const bool &bIsConflicts);
|
||||
|
||||
private:
|
||||
Dtk::Core::QSettingBackend *m_backend;
|
||||
DSettingsDialog *m_pSettingsDialog;
|
||||
static Settings *s_pSetting;
|
||||
DDialog *m_pDialog;
|
||||
};
|
||||
|
||||
#endif // SETTINGS_H
|
||||
|
|
|
@ -85,3 +85,8 @@ void CropGifDialog::setSelRect(QRectF rect) {
|
|||
m_h->setValue(h);
|
||||
emit selRectChanged(x, y, w, h);
|
||||
}
|
||||
|
||||
void CropGifDialog::closeEvent(QCloseEvent *event) {
|
||||
Q_UNUSED(event);
|
||||
emit crop(-1, -1, -1, -1);
|
||||
}
|
||||
|
|
|
@ -25,6 +25,9 @@ signals:
|
|||
void selRectChanged(int x, int y, int w, int h);
|
||||
void pressCancel();
|
||||
|
||||
protected:
|
||||
void closeEvent(QCloseEvent *event) override;
|
||||
|
||||
private:
|
||||
DButtonBox *btnbox;
|
||||
DSpinBox *m_x, *m_y, *m_w, *m_h;
|
||||
|
|
|
@ -34,6 +34,7 @@
|
|||
#include <QListWidgetItem>
|
||||
#include <QMessageBox>
|
||||
#include <QMimeData>
|
||||
#include <QProcess>
|
||||
#include <QRubberBand>
|
||||
#include <QSettings>
|
||||
#include <QShortcut>
|
||||
|
@ -584,6 +585,8 @@ MainWindow::MainWindow(DMainWindow *parent) : DMainWindow(parent) {
|
|||
CheckEnabled;
|
||||
editor->endCrop();
|
||||
this->setEditMode(true);
|
||||
if (x < 0 || y < 0 || w < 0 || h < 0)
|
||||
return;
|
||||
undo.push(new CropImageCommand(&gif, x, y, w, h));
|
||||
editor->fitPicEditor();
|
||||
});
|
||||
|
@ -632,27 +635,22 @@ MainWindow::MainWindow(DMainWindow *parent) : DMainWindow(parent) {
|
|||
ConnectShortCut(keycutpic, MainWindow::on_cutpic);
|
||||
ConnectShortCut(keyscaledelay, MainWindow::on_scaledelay);
|
||||
|
||||
m_settings = new Settings(this);
|
||||
|
||||
connect(m_settings, &Settings::sigAdjustQuality, this,
|
||||
[=](int v) { _quality = v; });
|
||||
connect(m_settings, &Settings::sigChangeWindowState,
|
||||
[=](QString mode) { _windowmode = mode; });
|
||||
|
||||
m_settings->applySetting();
|
||||
|
||||
if (_windowmode == "window_normal") {
|
||||
setWindowState(Qt::WindowState::WindowActive);
|
||||
} else if (_windowmode == "window_maximum") {
|
||||
setWindowState(Qt::WindowState::WindowMaximized);
|
||||
} else if (_windowmode == "window_minimum") {
|
||||
setWindowState(Qt::WindowState::WindowMinimized);
|
||||
} else {
|
||||
setWindowState(Qt::WindowState::WindowFullScreen);
|
||||
}
|
||||
|
||||
m_settings->loadWindowState(this);
|
||||
m_settings = Settings::instance();
|
||||
m_settings->loadWindowStatus(this);
|
||||
lastusedpath = m_settings->loadFileDialogCurrent();
|
||||
m_settingd.state = WinState(m_settings->windowState());
|
||||
switch (m_settingd.state) {
|
||||
case Normal:
|
||||
break;
|
||||
case Maximum:
|
||||
showMaximized();
|
||||
break;
|
||||
case Minimum:
|
||||
showMinimized();
|
||||
break;
|
||||
}
|
||||
m_settingd.cmdline = m_settings->reprocessingCmdline();
|
||||
m_settingd.terminal = m_settings->terminal();
|
||||
}
|
||||
|
||||
void MainWindow::openGif(QString filename) {
|
||||
|
@ -746,82 +744,15 @@ void MainWindow::showGifMessage(QString message) {
|
|||
}
|
||||
|
||||
bool MainWindow::saveGif(QString filename) {
|
||||
|
||||
GifEncoder gifsaver;
|
||||
auto size = gif.size();
|
||||
if (gifsaver.open(filename.toStdString(), uint16_t(size.width()),
|
||||
uint16_t(size.height()), _quality, false, 0)) {
|
||||
|
||||
if (gifsaver.open(filename, size.width(), size.height())) {
|
||||
QApplication::processEvents();
|
||||
|
||||
auto frames = gif.frames();
|
||||
auto pframe = frames.begin();
|
||||
auto eframe = frames.end();
|
||||
|
||||
auto img = pframe->image;
|
||||
|
||||
QApplication::processEvents();
|
||||
gifsaver.push(GifEncoder::PIXEL_FORMAT_RGBA, img.constBits(), 0, 0,
|
||||
img.width(), img.height(), pframe->delayTime / 10);
|
||||
|
||||
auto lframe = pframe;
|
||||
pframe++;
|
||||
for (; pframe != eframe; pframe++, lframe++) {
|
||||
img = pframe->image;
|
||||
auto limg = lframe->image;
|
||||
auto bpl = img.bytesPerLine();
|
||||
auto ls = img.height();
|
||||
|
||||
int x, y, x0, y0;
|
||||
for (y = 0; y < ls - 1; y++) {
|
||||
auto o = limg.constScanLine(y);
|
||||
auto d = img.constScanLine(y);
|
||||
if (memcmp(o, d, size_t(bpl))) {
|
||||
break;
|
||||
}
|
||||
QApplication::processEvents();
|
||||
}
|
||||
|
||||
for (y0 = ls - 1; y0 > y; y0--) {
|
||||
auto o = limg.constScanLine(y0);
|
||||
auto d = img.constScanLine(y0);
|
||||
if (memcmp(o, d, size_t(bpl))) {
|
||||
break;
|
||||
}
|
||||
QApplication::processEvents();
|
||||
}
|
||||
|
||||
QTransform trans;
|
||||
trans.rotate(90);
|
||||
|
||||
auto rimg = img.transformed(trans);
|
||||
auto rlimg = limg.transformed(trans);
|
||||
|
||||
bpl = rimg.bytesPerLine();
|
||||
ls = rimg.height();
|
||||
for (x = 0; x < ls - 1; x++) {
|
||||
auto o = rlimg.constScanLine(x);
|
||||
auto d = rimg.constScanLine(x);
|
||||
if (memcmp(o, d, size_t(bpl))) {
|
||||
break;
|
||||
}
|
||||
QApplication::processEvents();
|
||||
}
|
||||
for (x0 = ls - 1; x0 > x; x0--) {
|
||||
auto o = rlimg.constScanLine(x0);
|
||||
auto d = rimg.constScanLine(x0);
|
||||
if (memcmp(o, d, size_t(bpl))) {
|
||||
break;
|
||||
}
|
||||
QApplication::processEvents();
|
||||
}
|
||||
|
||||
auto timg = img.copy(adjustImageSize(x, y, x0 - x + 1, y0 - y + 1));
|
||||
auto &frames = gif.frames();
|
||||
for (auto &frame : frames) {
|
||||
gifsaver.push(frame.image, frame.delayTime / 10);
|
||||
QApplication::processEvents();
|
||||
gifsaver.push(GifEncoder::PIXEL_FORMAT_RGBA, timg.constBits(), x, y,
|
||||
timg.width(), timg.height(), pframe->delayTime / 10);
|
||||
}
|
||||
|
||||
gifsaver.close();
|
||||
undo.setClean();
|
||||
return true;
|
||||
|
@ -829,26 +760,6 @@ bool MainWindow::saveGif(QString filename) {
|
|||
return false;
|
||||
}
|
||||
|
||||
QRect MainWindow::adjustImageSize(int x, int y, int w, int h) {
|
||||
// 由于保存图像使用神经网络算法,图片有最小值限制,这里命名为 OFFSET
|
||||
// 这里为实验值
|
||||
#define OFFSET 32
|
||||
if (w <= OFFSET) {
|
||||
if (x - OFFSET > 0) {
|
||||
x -= OFFSET;
|
||||
}
|
||||
w += OFFSET;
|
||||
}
|
||||
|
||||
if (h <= OFFSET) {
|
||||
if (y - OFFSET > 0) {
|
||||
y -= OFFSET;
|
||||
}
|
||||
h += OFFSET;
|
||||
}
|
||||
return QRect(x, y, w, h);
|
||||
}
|
||||
|
||||
void MainWindow::on_new_frompics() {
|
||||
player->stop();
|
||||
if (ensureSafeClose()) {
|
||||
|
@ -1051,8 +962,10 @@ void MainWindow::on_saveas() {
|
|||
WaitingDialog d;
|
||||
d.start(tr("SaveAsGif"));
|
||||
if (saveGif(filename)) {
|
||||
DMessageManager::instance()->sendMessage(this, ICONRES("saveas"),
|
||||
tr("SaveAsSuccess"));
|
||||
d.setMessage(tr("Reprocessing"));
|
||||
if (reprocessImage(filename))
|
||||
DMessageManager::instance()->sendMessage(this, ICONRES("saveas"),
|
||||
tr("SaveAsSuccess"));
|
||||
curfilename = filename;
|
||||
} else {
|
||||
DMessageManager::instance()->sendMessage(this, ICONRES("saveas"),
|
||||
|
@ -1116,12 +1029,13 @@ void MainWindow::on_close() {
|
|||
}
|
||||
|
||||
void MainWindow::on_setting() {
|
||||
DSettingsDialog *dialog = new DSettingsDialog(this);
|
||||
m_settings->setSettingDialog(dialog);
|
||||
dialog->updateSettings(m_settings->settings);
|
||||
dialog->exec();
|
||||
delete dialog;
|
||||
m_settings->settings->sync();
|
||||
SettingDialog dialog(m_settingd, this);
|
||||
if (dialog.exec()) {
|
||||
m_settingd = dialog.getResult();
|
||||
m_settings->setWindowState(m_settingd.state);
|
||||
m_settings->setTerminal(m_settingd.terminal);
|
||||
m_settings->setReprocessingCmdline(m_settingd.cmdline);
|
||||
}
|
||||
}
|
||||
|
||||
void MainWindow::on_undo() {
|
||||
|
@ -1185,9 +1099,11 @@ void MainWindow::on_save() {
|
|||
WaitingDialog d;
|
||||
d.start(tr("SaveGif"));
|
||||
if (saveGif(curfilename)) {
|
||||
d.setMessage(tr("Reprocessing"));
|
||||
undo.setClean();
|
||||
DMessageManager::instance()->sendMessage(this, ICONRES("save"),
|
||||
tr("SaveSuccess"));
|
||||
if (reprocessImage(curfilename))
|
||||
DMessageManager::instance()->sendMessage(this, ICONRES("save"),
|
||||
tr("SaveSuccess"));
|
||||
} else {
|
||||
DMessageManager::instance()->sendMessage(this, ICONRES("save"), "SaveFail");
|
||||
}
|
||||
|
@ -1465,10 +1381,28 @@ void MainWindow::on_wiki() {
|
|||
"WingGifEditor/wiki/%E4%BB%8B%E7%BB%8D"));
|
||||
}
|
||||
|
||||
bool MainWindow::reprocessImage(QString filename) {
|
||||
if (m_settingd.cmdline.length()) {
|
||||
QFileInfo info(m_settingd.terminal);
|
||||
if (info.exists() && info.isExecutable()) {
|
||||
if (QProcess::execute(m_settingd.terminal,
|
||||
{"-c", m_settingd.cmdline.arg(filename)}) < 0) {
|
||||
QMessageBox::critical(this, tr("Error"), tr("ImagingError"));
|
||||
return false;
|
||||
}
|
||||
} else {
|
||||
DMessageManager::instance()->sendMessage(this, ICONRES("icon"),
|
||||
tr("InvaildTerminal"));
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
void MainWindow::closeEvent(QCloseEvent *event) {
|
||||
player->stop();
|
||||
if (ensureSafeClose()) {
|
||||
m_settings->saveWindowState(this);
|
||||
m_settings->saveWindowStatus(this);
|
||||
gif.close();
|
||||
event->accept();
|
||||
} else
|
||||
|
|
|
@ -5,9 +5,10 @@
|
|||
#include "Class/gifeditor.h"
|
||||
#include "Class/playgifmanager.h"
|
||||
#include "Class/settings.h"
|
||||
#include "Dialog/settingdialog.h"
|
||||
#include "Dialog/waitingdialog.h"
|
||||
#include "GifImage/decoder/gifdecoder.h"
|
||||
#include "GifImage/encoder/GifEncoder.h"
|
||||
#include "GifImage/encoder/gifencoder.h"
|
||||
#include "cropgifdialog.h"
|
||||
#include <DGraphicsView>
|
||||
#include <DLabel>
|
||||
|
@ -36,8 +37,7 @@ private:
|
|||
void setWritable(bool b);
|
||||
void showGifMessage(QString message = "");
|
||||
bool saveGif(QString filename);
|
||||
|
||||
QRect adjustImageSize(int x, int y, int w, int h);
|
||||
bool reprocessImage(QString filename);
|
||||
|
||||
private:
|
||||
void on_new_frompics();
|
||||
|
@ -115,8 +115,7 @@ private:
|
|||
QAction *undotool, *undomenu, *redotool, *redomenu;
|
||||
|
||||
Settings *m_settings;
|
||||
QString _windowmode;
|
||||
int _quality = 10;
|
||||
SettingResult m_settingd;
|
||||
|
||||
protected:
|
||||
void closeEvent(QCloseEvent *event) override;
|
||||
|
|
|
@ -0,0 +1,53 @@
|
|||
#include "settingdialog.h"
|
||||
#include <DDialogButtonBox>
|
||||
#include <DLabel>
|
||||
#include <QSettings>
|
||||
#include <QShortcut>
|
||||
|
||||
SettingDialog::SettingDialog(SettingResult res, DMainWindow *parent)
|
||||
: DDialog(parent) {
|
||||
setWindowTitle(tr("SettingDialog"));
|
||||
addContent(new DLabel(tr("WinState"), this));
|
||||
addSpacing(3);
|
||||
cbWinState = new DComboBox(this);
|
||||
cbWinState->addItems({tr("Normal"), tr("Maximum"), tr("Minimum")});
|
||||
cbWinState->setCurrentIndex(res.state);
|
||||
addContent(cbWinState);
|
||||
addSpacing(5);
|
||||
addContent(new DLabel(tr("Terminal"), this));
|
||||
addSpacing(3);
|
||||
leCmd = new DLineEdit(this);
|
||||
leCmd->setText(res.terminal);
|
||||
addContent(leCmd);
|
||||
addSpacing(5);
|
||||
addContent(new DLabel(tr("Reprocessing"), this));
|
||||
addSpacing(3);
|
||||
cmdline = new DLineEdit(this);
|
||||
cmdline->setText(res.cmdline);
|
||||
addContent(cmdline);
|
||||
addSpacing(20);
|
||||
auto dbbox = new DDialogButtonBox(
|
||||
DDialogButtonBox::Ok | DDialogButtonBox::Cancel, this);
|
||||
connect(dbbox, &DDialogButtonBox::accepted, this, &SettingDialog::on_accept);
|
||||
connect(dbbox, &DDialogButtonBox::rejected, this, &SettingDialog::on_reject);
|
||||
auto key = QKeySequence(Qt::Key_Return);
|
||||
auto s = new QShortcut(key, this);
|
||||
connect(s, &QShortcut::activated, this, &SettingDialog::on_accept);
|
||||
addContent(dbbox);
|
||||
}
|
||||
|
||||
SettingResult SettingDialog::getResult() { return m_res; }
|
||||
|
||||
void SettingDialog::on_accept() {
|
||||
m_res.state = WinState(cbWinState->currentIndex());
|
||||
m_res.terminal = leCmd->text().trimmed();
|
||||
m_res.cmdline = cmdline->text().trimmed();
|
||||
done(1);
|
||||
}
|
||||
|
||||
void SettingDialog::on_reject() { done(0); }
|
||||
|
||||
void SettingDialog::closeEvent(QCloseEvent *event) {
|
||||
Q_UNUSED(event);
|
||||
done(0);
|
||||
}
|
|
@ -0,0 +1,42 @@
|
|||
#ifndef SETTINGDIALOG_H
|
||||
#define SETTINGDIALOG_H
|
||||
|
||||
#include <DComboBox>
|
||||
#include <DDialog>
|
||||
#include <DLineEdit>
|
||||
#include <DMainWindow>
|
||||
#include <QObject>
|
||||
|
||||
DWIDGET_USE_NAMESPACE
|
||||
|
||||
enum WinState { Normal, Maximum, Minimum };
|
||||
|
||||
struct SettingResult {
|
||||
WinState state = Normal;
|
||||
QString terminal;
|
||||
QString cmdline;
|
||||
};
|
||||
|
||||
class SettingDialog : public DDialog {
|
||||
Q_OBJECT
|
||||
public:
|
||||
explicit SettingDialog(SettingResult res, DMainWindow *parent = nullptr);
|
||||
|
||||
SettingResult getResult();
|
||||
|
||||
private:
|
||||
void on_accept();
|
||||
void on_reject();
|
||||
|
||||
protected:
|
||||
void closeEvent(QCloseEvent *event) override;
|
||||
|
||||
private:
|
||||
DLineEdit *leCmd;
|
||||
DLineEdit *cmdline;
|
||||
DComboBox *cbWinState;
|
||||
|
||||
SettingResult m_res;
|
||||
};
|
||||
|
||||
#endif // SETTINGDIALOG_H
|
|
@ -41,3 +41,8 @@ void WaitingDialog::stop() {
|
|||
pro->stop();
|
||||
animation->stop();
|
||||
}
|
||||
|
||||
void WaitingDialog::setMessage(QString message) {
|
||||
info->setText(message);
|
||||
QApplication::processEvents();
|
||||
}
|
||||
|
|
|
@ -17,6 +17,7 @@ public:
|
|||
|
||||
void start(QString message);
|
||||
void stop();
|
||||
void setMessage(QString message);
|
||||
|
||||
private:
|
||||
DWaterProgress *pro;
|
||||
|
|
|
@ -1,11 +0,0 @@
|
|||
SOURCES += \
|
||||
$$PWD/decoder/gifdecoder.cpp \
|
||||
$$PWD/encoder/GifEncoder.cpp \
|
||||
$$PWD/encoder/algorithm/NeuQuant.cpp
|
||||
|
||||
HEADERS += \
|
||||
$$PWD/decoder/gifdecoder.h \
|
||||
$$PWD/encoder/GifEncoder.h \
|
||||
$$PWD/encoder/algorithm/NeuQuant.h
|
||||
|
||||
include($$PWD/giflib.pri)
|
|
@ -1,288 +0,0 @@
|
|||
//
|
||||
// Created by xiaozhuai on 2020/12/20.
|
||||
//
|
||||
|
||||
#include "GifEncoder.h"
|
||||
#include "algorithm/NeuQuant.h"
|
||||
#include "gif_lib.h"
|
||||
#include <cstdlib>
|
||||
#include <cstring>
|
||||
#include <exception>
|
||||
#include <stdexcept>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include <QApplication>
|
||||
|
||||
#define GifAddExtensionBlockFor(a, func, len, data) \
|
||||
GifAddExtensionBlock(&((a)->ExtensionBlockCount), &((a)->ExtensionBlocks), \
|
||||
func, len, data)
|
||||
|
||||
static void getColorMap(uint8_t *colorMap, const uint8_t *pixels, int nPixels,
|
||||
int quality) {
|
||||
initnet(pixels, nPixels * 3, quality);
|
||||
learn();
|
||||
unbiasnet();
|
||||
inxbuild();
|
||||
getcolourmap(colorMap);
|
||||
}
|
||||
|
||||
static void getRasterBits(uint8_t *rasterBits, const uint8_t *pixels,
|
||||
int nPixels) {
|
||||
for (int i = 0; i < nPixels; ++i) {
|
||||
rasterBits[i] =
|
||||
u_char(inxsearch(pixels[i * 3], pixels[i * 3 + 1], pixels[i * 3 + 2]));
|
||||
}
|
||||
}
|
||||
|
||||
inline void RGB2BGR(uint8_t *dst, const uint8_t *src, int width, int height) {
|
||||
for (const uint8_t *dstEnd = dst + width * height * 3; dst < dstEnd;
|
||||
src += 3) {
|
||||
*(dst++) = *(src + 2);
|
||||
*(dst++) = *(src + 1);
|
||||
*(dst++) = *(src);
|
||||
}
|
||||
}
|
||||
|
||||
inline void BGRA2BGR(uint8_t *dst, const uint8_t *src, int width, int height) {
|
||||
for (const uint8_t *dstEnd = dst + width * height * 3; dst < dstEnd;
|
||||
src += 4) {
|
||||
*(dst++) = *(src);
|
||||
*(dst++) = *(src + 1);
|
||||
*(dst++) = *(src + 2);
|
||||
}
|
||||
}
|
||||
|
||||
inline void RGBA2BGR(uint8_t *dst, const uint8_t *src, int width, int height) {
|
||||
for (const uint8_t *dstEnd = dst + width * height * 3; dst < dstEnd;
|
||||
src += 4) {
|
||||
*(dst++) = *(src + 2);
|
||||
*(dst++) = *(src + 1);
|
||||
*(dst++) = *(src);
|
||||
}
|
||||
}
|
||||
|
||||
static bool convertToBGR(GifEncoder::PixelFormat format, uint8_t *dst,
|
||||
const uint8_t *src, int width, int height) {
|
||||
switch (format) {
|
||||
case GifEncoder::PIXEL_FORMAT_BGR:
|
||||
memcpy(dst, src, size_t(width * height * 3));
|
||||
break;
|
||||
case GifEncoder::PIXEL_FORMAT_RGB:
|
||||
RGB2BGR(dst, src, width, height);
|
||||
break;
|
||||
case GifEncoder::PIXEL_FORMAT_BGRA:
|
||||
BGRA2BGR(dst, src, width, height);
|
||||
break;
|
||||
case GifEncoder::PIXEL_FORMAT_RGBA:
|
||||
RGBA2BGR(dst, src, width, height);
|
||||
break;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
bool GifEncoder::open(const std::string &file, int width, int height,
|
||||
int quality, bool useGlobalColorMap, int16_t loop,
|
||||
int preAllocSize) {
|
||||
if (m_gifFile != nullptr) {
|
||||
return false;
|
||||
}
|
||||
|
||||
int error;
|
||||
m_gifFile = EGifOpenFileName(file.c_str(), false, &error);
|
||||
if (!m_gifFile) {
|
||||
return false;
|
||||
}
|
||||
|
||||
m_quality = quality;
|
||||
m_useGlobalColorMap = useGlobalColorMap;
|
||||
|
||||
reset();
|
||||
|
||||
if (preAllocSize > 0) {
|
||||
m_framePixels = reinterpret_cast<uint8_t *>(malloc(ulong(preAllocSize)));
|
||||
m_allocSize = preAllocSize;
|
||||
}
|
||||
|
||||
m_gifFile->SWidth = width;
|
||||
m_gifFile->SHeight = height;
|
||||
m_gifFile->SColorResolution = 8;
|
||||
m_gifFile->SBackGroundColor = 0;
|
||||
m_gifFile->SColorMap = nullptr;
|
||||
|
||||
uint8_t appExt[11] = {'N', 'E', 'T', 'S', 'C', 'A', 'P', 'E', '2', '.', '0'};
|
||||
uint8_t appExtSubBlock[3] = {
|
||||
0x01, // hex 0x01
|
||||
0x00, 0x00 // little-endian short. The number of times the loop should be
|
||||
// executed.
|
||||
};
|
||||
memcpy(appExtSubBlock + 1, &loop, sizeof(loop));
|
||||
|
||||
GifAddExtensionBlockFor(m_gifFile, APPLICATION_EXT_FUNC_CODE, sizeof(appExt),
|
||||
appExt);
|
||||
GifAddExtensionBlockFor(m_gifFile, CONTINUE_EXT_FUNC_CODE,
|
||||
sizeof(appExtSubBlock), appExtSubBlock);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
bool GifEncoder::push(PixelFormat format, const uint8_t *frame, int x, int y,
|
||||
int width, int height, int delay) {
|
||||
if (m_gifFile == nullptr) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (frame == nullptr) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (m_useGlobalColorMap) {
|
||||
if (isFirstFrame()) {
|
||||
m_frameWidth = width;
|
||||
m_frameHeight = height;
|
||||
} else {
|
||||
if (m_frameWidth != width || m_frameHeight != height) {
|
||||
throw std::runtime_error(
|
||||
"Frame size must be same when use global color map!");
|
||||
}
|
||||
}
|
||||
|
||||
int needSize = width * height * 3 * (m_frameCount + 1);
|
||||
if (m_allocSize < needSize) {
|
||||
m_framePixels =
|
||||
reinterpret_cast<uint8_t *>(realloc(m_framePixels, ulong(needSize)));
|
||||
m_allocSize = needSize;
|
||||
}
|
||||
auto *pixels = m_framePixels + width * height * 3 * m_frameCount;
|
||||
convertToBGR(format, pixels, frame, width, height);
|
||||
|
||||
frameInfo finfo;
|
||||
finfo.x = x;
|
||||
finfo.y = y;
|
||||
finfo.w = width;
|
||||
finfo.h = height;
|
||||
m_allFrames.push_back(finfo);
|
||||
|
||||
} else {
|
||||
int needSize = width * height * 3;
|
||||
if (m_allocSize < needSize) {
|
||||
m_framePixels =
|
||||
reinterpret_cast<uint8_t *>(realloc(m_framePixels, ulong(needSize)));
|
||||
m_allocSize = needSize;
|
||||
// printf("realloc 2\n");
|
||||
}
|
||||
|
||||
auto *pixels = m_framePixels;
|
||||
convertToBGR(format, pixels, frame, width, height);
|
||||
|
||||
auto *colorMap = GifMakeMapObject(256, nullptr);
|
||||
getColorMap(reinterpret_cast<uint8_t *>(colorMap->Colors), pixels,
|
||||
width * height, m_quality);
|
||||
|
||||
auto *rasterBits =
|
||||
reinterpret_cast<GifByteType *>(malloc(ulong(width * height)));
|
||||
getRasterBits(reinterpret_cast<uint8_t *>(rasterBits), pixels,
|
||||
width * height);
|
||||
|
||||
encodeFrame(x, y, width, height, delay, colorMap, rasterBits);
|
||||
}
|
||||
|
||||
m_frameCount++;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
bool GifEncoder::close() {
|
||||
if (m_gifFile == nullptr) {
|
||||
return false;
|
||||
}
|
||||
|
||||
ColorMapObject *globalColorMap = nullptr;
|
||||
|
||||
if (m_useGlobalColorMap) {
|
||||
globalColorMap = GifMakeMapObject(256, nullptr);
|
||||
getColorMap(reinterpret_cast<uint8_t *>(globalColorMap->Colors),
|
||||
m_framePixels, m_frameWidth * m_frameHeight * m_frameCount,
|
||||
m_quality);
|
||||
m_gifFile->SColorMap = globalColorMap;
|
||||
|
||||
for (int i = 0; i < m_frameCount; ++i) {
|
||||
auto *pixels = m_framePixels + m_frameWidth * m_frameHeight * 3 * i;
|
||||
auto *rasterBits = reinterpret_cast<GifByteType *>(
|
||||
malloc(ulong(m_frameWidth * m_frameHeight)));
|
||||
getRasterBits(reinterpret_cast<uint8_t *>(rasterBits), pixels,
|
||||
m_frameWidth * m_frameHeight);
|
||||
|
||||
auto &finfo = m_allFrames[ulong(i)];
|
||||
|
||||
encodeFrame(finfo.x, finfo.y, finfo.w, finfo.h, finfo.delay, nullptr,
|
||||
rasterBits);
|
||||
QApplication::processEvents();
|
||||
}
|
||||
}
|
||||
|
||||
int extCount = m_gifFile->ExtensionBlockCount;
|
||||
auto *extBlocks = m_gifFile->ExtensionBlocks;
|
||||
|
||||
int savedImageCount = m_gifFile->ImageCount;
|
||||
auto *savedImages = m_gifFile->SavedImages;
|
||||
|
||||
if (EGifSpew(m_gifFile) == GIF_ERROR) {
|
||||
EGifCloseFile(reinterpret_cast<GifFileType *>(m_gifFile));
|
||||
m_gifFile = nullptr;
|
||||
return false;
|
||||
}
|
||||
|
||||
if (globalColorMap != nullptr) {
|
||||
GifFreeMapObject(globalColorMap);
|
||||
}
|
||||
|
||||
GifFreeExtensions(&extCount, &extBlocks);
|
||||
for (auto *sp = savedImages; sp < savedImages + savedImageCount; sp++) {
|
||||
if (sp->ImageDesc.ColorMap != nullptr) {
|
||||
GifFreeMapObject(sp->ImageDesc.ColorMap);
|
||||
sp->ImageDesc.ColorMap = nullptr;
|
||||
}
|
||||
|
||||
if (sp->RasterBits != nullptr) {
|
||||
free(sp->RasterBits);
|
||||
sp->RasterBits = nullptr;
|
||||
}
|
||||
|
||||
GifFreeExtensions(&sp->ExtensionBlockCount, &sp->ExtensionBlocks);
|
||||
}
|
||||
free(savedImages);
|
||||
|
||||
m_gifFile = nullptr;
|
||||
|
||||
reset();
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
void GifEncoder::encodeFrame(int x, int y, int width, int height, int delay,
|
||||
void *colorMap, void *rasterBits) {
|
||||
auto *gifImage = GifMakeSavedImage(m_gifFile, nullptr);
|
||||
|
||||
gifImage->ImageDesc.Left = x;
|
||||
gifImage->ImageDesc.Top = y;
|
||||
gifImage->ImageDesc.Width = width;
|
||||
gifImage->ImageDesc.Height = height;
|
||||
gifImage->ImageDesc.Interlace = false;
|
||||
gifImage->ImageDesc.ColorMap = reinterpret_cast<ColorMapObject *>(colorMap);
|
||||
gifImage->RasterBits = reinterpret_cast<GifByteType *>(rasterBits);
|
||||
gifImage->ExtensionBlockCount = 0;
|
||||
gifImage->ExtensionBlocks = nullptr;
|
||||
|
||||
GraphicsControlBlock gcb;
|
||||
gcb.DisposalMode = DISPOSE_DO_NOT;
|
||||
gcb.UserInputFlag = false;
|
||||
gcb.DelayTime = delay;
|
||||
gcb.TransparentColor = NO_TRANSPARENT_COLOR;
|
||||
uint8_t gcbBytes[4];
|
||||
EGifGCBToExtension(&gcb, gcbBytes);
|
||||
GifAddExtensionBlockFor(gifImage, GRAPHICS_EXT_FUNC_CODE, sizeof(gcbBytes),
|
||||
gcbBytes);
|
||||
}
|
|
@ -1,103 +0,0 @@
|
|||
//
|
||||
// Created by xiaozhuai on 2020/12/20.
|
||||
//
|
||||
|
||||
#ifndef GIF_GIFENCODER_H
|
||||
#define GIF_GIFENCODER_H
|
||||
|
||||
#include "gif_lib.h"
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
class GifEncoder {
|
||||
public:
|
||||
enum PixelFormat {
|
||||
PIXEL_FORMAT_UNKNOWN = 0,
|
||||
PIXEL_FORMAT_BGR = 1,
|
||||
PIXEL_FORMAT_RGB = 2,
|
||||
PIXEL_FORMAT_BGRA = 3,
|
||||
PIXEL_FORMAT_RGBA = 4,
|
||||
};
|
||||
|
||||
public:
|
||||
GifEncoder() = default;
|
||||
|
||||
/**
|
||||
* create gif file
|
||||
*
|
||||
* @param file file path
|
||||
* @param width gif width
|
||||
* @param height gif height
|
||||
* @param quality 1..30, 1 is best
|
||||
* @param useGlobalColorMap
|
||||
* @param loop loop count, 0 is endless
|
||||
* @param For better performance, it's suggested to set preAllocSize. If you
|
||||
* can't determine it, set to 0. If use global color map, all frames size must
|
||||
* be same, and preAllocSize = width * height * 3 * nFrame If use local color
|
||||
* map, preAllocSize = MAX(width * height) * 3
|
||||
* @return
|
||||
*/
|
||||
bool open(const std::string &file, int width, int height, int quality,
|
||||
bool useGlobalColorMap, int16_t loop, int preAllocSize = 0);
|
||||
|
||||
/**
|
||||
* add frame
|
||||
*
|
||||
* @param format pixel format
|
||||
* @param frame frame data
|
||||
* @param width frame width
|
||||
* @param height frame height
|
||||
* @param delay delay time 0.01s
|
||||
* @return
|
||||
*/
|
||||
bool push(PixelFormat format, const uint8_t *frame, int x, int y, int width,
|
||||
int height, int delay);
|
||||
|
||||
/**
|
||||
* close gif file
|
||||
*
|
||||
* @return
|
||||
*/
|
||||
bool close();
|
||||
|
||||
private:
|
||||
inline bool isFirstFrame() const { return m_frameCount == 0; }
|
||||
|
||||
inline void reset() {
|
||||
if (m_framePixels != nullptr) {
|
||||
free(m_framePixels);
|
||||
m_framePixels = nullptr;
|
||||
}
|
||||
m_allocSize = 0;
|
||||
m_allFrames.clear();
|
||||
m_frameCount = 0;
|
||||
m_frameWidth = -1;
|
||||
m_frameHeight = -1;
|
||||
}
|
||||
|
||||
void encodeFrame(int x, int y, int width, int height, int delay,
|
||||
void *colorMap, void *rasterBits);
|
||||
|
||||
private:
|
||||
struct frameInfo {
|
||||
int delay;
|
||||
int x;
|
||||
int y;
|
||||
int w;
|
||||
int h;
|
||||
};
|
||||
|
||||
GifFileType *m_gifFile = nullptr;
|
||||
|
||||
int m_quality = 10;
|
||||
bool m_useGlobalColorMap = false;
|
||||
|
||||
uint8_t *m_framePixels = nullptr;
|
||||
int m_allocSize = 0;
|
||||
std::vector<frameInfo> m_allFrames{};
|
||||
int m_frameCount = 0;
|
||||
int m_frameWidth = -1;
|
||||
int m_frameHeight = -1;
|
||||
};
|
||||
|
||||
#endif // GIF_GIFENCODER_H
|
|
@ -0,0 +1,21 @@
|
|||
MIT License
|
||||
|
||||
Copyright (c) 2021 Daniel Löbl
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
|
@ -0,0 +1,72 @@
|
|||
## CGIF, a GIF encoder written in C
|
||||
|
||||
A fast and lightweight GIF encoder that can create GIF animations and images. Summary of the main features:
|
||||
- user-defined global or local color-palette with up to 256 colors (limit of the GIF format)
|
||||
- size-optimizations for GIF animations:
|
||||
- option to set a pixel to transparent if it has identical color in the previous frame (transparency optimization)
|
||||
- do encoding just for the rectangular area that differs from the previous frame (width/height optimization)
|
||||
- fast: a GIF with 256 colors and 1024x1024 pixels can be created in below 50 ms even on a minimalistic system
|
||||
- MIT license (permissive)
|
||||
- different options for GIF animations: static image, N repetitions, infinite repetitions
|
||||
- additional source-code for verifying the encoder after making changes
|
||||
- user-defined delay time from one frame to the next (can be set independently for each frame)
|
||||
- source-code conforms to the C99 standard
|
||||
|
||||
## Examples
|
||||
To get started, we suggest that you have a look at our code examples. ```examples/cgif_example_video.c``` is an example that creates a GIF animation. ```examples/cgif_example.c``` is an example for a static GIF image.
|
||||
|
||||
## Overview
|
||||
To get an overview of the API, we recommend having a look at our wiki (https://github.com/dloebl/cgif/wiki/General-API) where types and functions are described. The corresponding implementations can be found in ```src/cgif.c``` and ```src/cgif_raw.c```. Here the most important types and functions:
|
||||
|
||||
```C
|
||||
// These are the four struct types that contain all GIF data and parameters:
|
||||
typedef CGIF_Config // global cofinguration parameters of the GIF
|
||||
typedef CGIF_FrameConfig // local configuration parameters for a frame
|
||||
typedef CGIF // struct for the full GIF
|
||||
typedef CGIF_Frame // struct for a single frame
|
||||
|
||||
// The user needs only these three functions to create a GIF image:
|
||||
CGIF* cgif_newgif (CGIF_Config* pConfig); // creates a new GIF
|
||||
int cgif_addframe (CGIF* pGIF, CGIF_FrameConfig* pConfig); // adds a frame to an existing GIF
|
||||
int cgif_close (CGIF* pGIF); // close the created file and free memory
|
||||
```
|
||||
|
||||
With our encoder you can create animated or static GIFs, you can or cannot use certain optimizations, and so on. You can switch between all these different options easily using the two attributes ```attrFlags``` and ```genFlags``` in the configurations ```CGIF_Config``` and ```CGIF_FrameConfig```. These attributes are of type ```uint32_t``` and bundle yes/no-options with a bit-wise logic. So far only a few of the 32 bits are used leaving space to include further functionalities ensuring backward compatibility. We provide the following flag settings which can be combined by bit-wise or-operations:
|
||||
```C
|
||||
CGIF_ATTR_IS_ANIMATED // make an animated GIF (default is non-animated GIF)
|
||||
CGIF_ATTR_NO_GLOBAL_TABLE // disable global color table (global color table is default)
|
||||
CGIF_ATTR_HAS_TRANSPARENCY // first entry in color table contains transparency (alpha channel)
|
||||
CGIF_ATTR_NO_LOOP // run GIF animation only one time. numLoops is ignored (no repetitions)
|
||||
CGIF_FRAME_ATTR_USE_LOCAL_TABLE // use a local color table for a frame (not used by default)
|
||||
CGIF_FRAME_ATTR_HAS_ALPHA // frame contains alpha channel (index set via transIndex field)
|
||||
CGIF_FRAME_ATTR_HAS_SET_TRANS // transparency setting provided by user (transIndex field)
|
||||
CGIF_FRAME_ATTR_INTERLACED // encode frame interlaced
|
||||
CGIF_FRAME_GEN_USE_TRANSPARENCY // use transparency optimization (size optimization)
|
||||
CGIF_FRAME_GEN_USE_DIFF_WINDOW // do encoding just for the sub-window that changed (size optimization)
|
||||
```
|
||||
If you didn't understand the point of ```attrFlags``` and ```genFlags``` and the flags, please don't worry. The example files ```examples/cgif_example.c``` and ```examples/cgif_example_video.c``` are all you need to get started and the used default settings for ```attrFlags``` and ```genFlags``` cover most cases quite well.
|
||||
|
||||
## Compiling the example
|
||||
An example can be compiled and tested simply by:
|
||||
```
|
||||
$ c99 -o cgif_example -Iinc examples/cgif_example_video.c src/cgif.c src/cgif_raw.c
|
||||
$ ./cgif_example
|
||||
|
||||
```
|
||||
|
||||
## Validating the encoder
|
||||
In the folder ```tests```, we provide several testing routines that you can run via the script ```tests/performtests.sh```. To perform the tests you need to install the programs [ImageMagick](https://github.com/ImageMagick/ImageMagick), [gifsicle](https://github.com/kohler/gifsicle) and [tcc (tiny c compiler)](https://bellard.org/tcc/).
|
||||
With the provided tests you can validate that the encoder still generates correct GIF files after making changes on the encoder itself.
|
||||
|
||||
## Further explanations
|
||||
The GIF format employs the [Lempel-Ziv-Welch (LZW)](https://en.wikipedia.org/wiki/Lempel%E2%80%93Ziv%E2%80%93Welch) algorithm for image compression. If you are interested in details of the GIF format, please have a look at the official GIF documentation (https://www.w3.org/Graphics/GIF/spec-gif89a.txt).
|
||||
|
||||
## Versioning scheme
|
||||
Releases of cgif follow the semantic versioning scheme as described here: [semver.org](https://semver.org/)
|
||||
|
||||
The following additional guarantees are provided:
|
||||
* Public API of versions 0.x.x are stable.
|
||||
|
||||
## License
|
||||
Licensed under the MIT license (permissive).
|
||||
For more details please see ```LICENSE```
|
|
@ -1,436 +0,0 @@
|
|||
/* NeuQuant Neural-Net Quantization Algorithm
|
||||
* ------------------------------------------
|
||||
*
|
||||
* Copyright (c) 1994 Anthony Dekker
|
||||
*
|
||||
* NEUQUANT Neural-Net quantization algorithm by Anthony Dekker, 1994.
|
||||
* See "Kohonen neural networks for optimal colour quantization"
|
||||
* in "Network: Computation in Neural Systems" Vol. 5 (1994) pp 351-367.
|
||||
* for a discussion of the algorithm.
|
||||
* See also http://members.ozemail.com.au/~dekker/NEUQUANT.HTML
|
||||
*
|
||||
* Any party obtaining a copy of these files from the author, directly or
|
||||
* indirectly, is granted, free of charge, a full and unrestricted irrevocable,
|
||||
* world-wide, paid up, royalty-free, nonexclusive right and license to deal
|
||||
* in this software and documentation files (the "Software"), including without
|
||||
* limitation the rights to use, copy, modify, merge, publish, distribute, sublicense,
|
||||
* and/or sell copies of the Software, and to permit persons who receive
|
||||
* copies from any such party to do so, with the only requirement being
|
||||
* that this copyright notice remain intact.
|
||||
*/
|
||||
|
||||
|
||||
#include "NeuQuant.h"
|
||||
|
||||
|
||||
/* Network Definitions
|
||||
------------------- */
|
||||
|
||||
#define maxnetpos 255
|
||||
#define netbiasshift 4 /* bias for colour values */
|
||||
#define ncycles 100 /* no. of learning cycles */
|
||||
|
||||
/* defs for freq and bias */
|
||||
#define intbiasshift 16 /* bias for fractions */
|
||||
#define intbias 65536
|
||||
#define gammashift 10 /* gamma = 1024 */
|
||||
#define gamma 1024
|
||||
#define betashift 10
|
||||
#define beta 64 /* beta = 1/1024 */
|
||||
#define betagamma 65536
|
||||
|
||||
/* defs for decreasing radius factor */
|
||||
#define initrad 32 /* for 256 cols, radius starts */
|
||||
#define radiusbiasshift 6 /* at 32.0 biased by 6 bits */
|
||||
#define radiusbias 64
|
||||
#define initradius 2048 /* and decreases by a */
|
||||
#define radiusdec 30 /* factor of 1/30 each cycle */
|
||||
|
||||
/* defs for decreasing alpha factor */
|
||||
#define alphabiasshift 10 /* alpha starts at 1.0 */
|
||||
#define initalpha 1024
|
||||
int alphadec; /* biased by 10 bits */
|
||||
|
||||
/* radbias and alpharadbias used for radpower calculation */
|
||||
#define radbiasshift 8
|
||||
#define radbias 256
|
||||
#define alpharadbshift 18
|
||||
#define alpharadbias 262144
|
||||
|
||||
|
||||
/* Types and Global Variables
|
||||
-------------------------- */
|
||||
|
||||
static const unsigned char *thepicture; /* the input image itself */
|
||||
static int lengthcount; /* lengthcount = H*W*3 */
|
||||
|
||||
static int samplefac; /* sampling factor 1..30 */
|
||||
|
||||
|
||||
typedef int pixel[4]; /* BGRc */
|
||||
static pixel network[netsize]; /* the network itself */
|
||||
|
||||
static int netindex[256]; /* for network lookup - really 256 */
|
||||
|
||||
static int bias[netsize]; /* bias and freq arrays for learning */
|
||||
static int freq[netsize];
|
||||
static int radpower[initrad]; /* radpower for precomputation */
|
||||
|
||||
|
||||
int getNetwork(int i, int j) {
|
||||
return network[i][j];
|
||||
}
|
||||
|
||||
/* Initialise network in range (0,0,0) to (255,255,255) and set parameters
|
||||
----------------------------------------------------------------------- */
|
||||
|
||||
void initnet(const unsigned char *thepic, int len, int sample) {
|
||||
int i;
|
||||
int *p;
|
||||
|
||||
thepicture = thepic;
|
||||
lengthcount = len;
|
||||
samplefac = sample;
|
||||
|
||||
for (i = 0; i < netsize; i++) {
|
||||
p = network[i];
|
||||
p[0] = p[1] = p[2] = (i << (netbiasshift + 8)) / netsize;
|
||||
freq[i] = intbias / netsize; /* 1/netsize */
|
||||
bias[i] = 0;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/* Unbias network to give byte values 0..255 and record position i to prepare for sort
|
||||
----------------------------------------------------------------------------------- */
|
||||
|
||||
void unbiasnet() {
|
||||
int i, j, temp;
|
||||
|
||||
for (i = 0; i < netsize; i++) {
|
||||
for (j = 0; j < 3; j++) {
|
||||
/* OLD CODE: network[i][j] >>= netbiasshift; */
|
||||
/* Fix based on bug report by Juergen Weigert jw@suse.de */
|
||||
temp = (network[i][j] + (1 << (netbiasshift - 1))) >> netbiasshift;
|
||||
if (temp > 255) temp = 255;
|
||||
network[i][j] = temp;
|
||||
}
|
||||
network[i][3] = i; /* record colour no */
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/* Output colour map
|
||||
----------------- */
|
||||
|
||||
void writecolourmap(FILE *f) {
|
||||
int i, j;
|
||||
|
||||
for (i = 2; i >= 0; i--)
|
||||
for (j = 0; j < netsize; j++)
|
||||
putc(network[j][i], f);
|
||||
}
|
||||
|
||||
void getcolourmap(uint8_t *colorMap) {
|
||||
int *index = new int[netsize];
|
||||
for (int i = 0; i < netsize; i++)
|
||||
index[network[i][3]] = i;
|
||||
|
||||
int k = 0;
|
||||
for (int i = 0; i < netsize; i++) {
|
||||
int j = index[i];
|
||||
colorMap[k++] = network[j][2];
|
||||
colorMap[k++] = network[j][1];
|
||||
colorMap[k++] = network[j][0];
|
||||
}
|
||||
|
||||
delete[] index;
|
||||
}
|
||||
|
||||
|
||||
/* Insertion sort of network and building of netindex[0..255] (to do after unbias)
|
||||
------------------------------------------------------------------------------- */
|
||||
|
||||
void inxbuild() {
|
||||
int i, j, smallpos, smallval;
|
||||
int *p, *q;
|
||||
int previouscol, startpos;
|
||||
|
||||
previouscol = 0;
|
||||
startpos = 0;
|
||||
for (i = 0; i < netsize; i++) {
|
||||
p = network[i];
|
||||
smallpos = i;
|
||||
smallval = p[1]; /* index on g */
|
||||
/* find smallest in i..netsize-1 */
|
||||
for (j = i + 1; j < netsize; j++) {
|
||||
q = network[j];
|
||||
if (q[1] < smallval) { /* index on g */
|
||||
smallpos = j;
|
||||
smallval = q[1]; /* index on g */
|
||||
}
|
||||
}
|
||||
q = network[smallpos];
|
||||
/* swap p (i) and q (smallpos) entries */
|
||||
if (i != smallpos) {
|
||||
j = q[0];
|
||||
q[0] = p[0];
|
||||
p[0] = j;
|
||||
j = q[1];
|
||||
q[1] = p[1];
|
||||
p[1] = j;
|
||||
j = q[2];
|
||||
q[2] = p[2];
|
||||
p[2] = j;
|
||||
j = q[3];
|
||||
q[3] = p[3];
|
||||
p[3] = j;
|
||||
}
|
||||
/* smallval entry is now in position i */
|
||||
if (smallval != previouscol) {
|
||||
netindex[previouscol] = (startpos + i) >> 1;
|
||||
for (j = previouscol + 1; j < smallval; j++) netindex[j] = i;
|
||||
previouscol = smallval;
|
||||
startpos = i;
|
||||
}
|
||||
}
|
||||
netindex[previouscol] = (startpos + maxnetpos) >> 1;
|
||||
for (j = previouscol + 1; j < 256; j++) netindex[j] = maxnetpos; /* really 256 */
|
||||
}
|
||||
|
||||
|
||||
/* Search for BGR values 0..255 (after net is unbiased) and return colour index
|
||||
---------------------------------------------------------------------------- */
|
||||
|
||||
int inxsearch(int b, int g, int r) {
|
||||
int i, j, dist, a, bestd;
|
||||
int *p;
|
||||
int best;
|
||||
|
||||
bestd = 1000; /* biggest possible dist is 256*3 */
|
||||
best = -1;
|
||||
i = netindex[g]; /* index on g */
|
||||
j = i - 1; /* start at netindex[g] and work outwards */
|
||||
|
||||
while ((i < netsize) || (j >= 0)) {
|
||||
if (i < netsize) {
|
||||
p = network[i];
|
||||
dist = p[1] - g; /* inx key */
|
||||
if (dist >= bestd) i = netsize; /* stop iter */
|
||||
else {
|
||||
i++;
|
||||
if (dist < 0) dist = -dist;
|
||||
a = p[0] - b;
|
||||
if (a < 0) a = -a;
|
||||
dist += a;
|
||||
if (dist < bestd) {
|
||||
a = p[2] - r;
|
||||
if (a < 0) a = -a;
|
||||
dist += a;
|
||||
if (dist < bestd) {
|
||||
bestd = dist;
|
||||
best = p[3];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (j >= 0) {
|
||||
p = network[j];
|
||||
dist = g - p[1]; /* inx key - reverse dif */
|
||||
if (dist >= bestd) j = -1; /* stop iter */
|
||||
else {
|
||||
j--;
|
||||
if (dist < 0) dist = -dist;
|
||||
a = p[0] - b;
|
||||
if (a < 0) a = -a;
|
||||
dist += a;
|
||||
if (dist < bestd) {
|
||||
a = p[2] - r;
|
||||
if (a < 0) a = -a;
|
||||
dist += a;
|
||||
if (dist < bestd) {
|
||||
bestd = dist;
|
||||
best = p[3];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return (best);
|
||||
}
|
||||
|
||||
|
||||
/* Search for biased BGR values
|
||||
---------------------------- */
|
||||
|
||||
int contest(int b, int g, int r) {
|
||||
/* finds closest neuron (min dist) and updates freq */
|
||||
/* finds best neuron (min dist-bias) and returns position */
|
||||
/* for frequently chosen neurons, freq[i] is high and bias[i] is negative */
|
||||
/* bias[i] = gamma*((1/netsize)-freq[i]) */
|
||||
|
||||
int i, dist, a, biasdist, betafreq;
|
||||
int bestpos, bestbiaspos, bestd, bestbiasd;
|
||||
int *p, *f, *n;
|
||||
|
||||
bestd = ~(((int) 1) << 31);
|
||||
bestbiasd = bestd;
|
||||
bestpos = -1;
|
||||
bestbiaspos = bestpos;
|
||||
p = bias;
|
||||
f = freq;
|
||||
|
||||
for (i = 0; i < netsize; i++) {
|
||||
n = network[i];
|
||||
dist = n[0] - b;
|
||||
if (dist < 0) dist = -dist;
|
||||
a = n[1] - g;
|
||||
if (a < 0) a = -a;
|
||||
dist += a;
|
||||
a = n[2] - r;
|
||||
if (a < 0) a = -a;
|
||||
dist += a;
|
||||
if (dist < bestd) {
|
||||
bestd = dist;
|
||||
bestpos = i;
|
||||
}
|
||||
biasdist = dist - ((*p) >> (intbiasshift - netbiasshift));
|
||||
if (biasdist < bestbiasd) {
|
||||
bestbiasd = biasdist;
|
||||
bestbiaspos = i;
|
||||
}
|
||||
betafreq = (*f >> betashift);
|
||||
*f++ -= betafreq;
|
||||
*p++ += (betafreq << gammashift);
|
||||
}
|
||||
freq[bestpos] += beta;
|
||||
bias[bestpos] -= betagamma;
|
||||
return (bestbiaspos);
|
||||
}
|
||||
|
||||
|
||||
/* Move neuron i towards biased (b,g,r) by factor alpha
|
||||
---------------------------------------------------- */
|
||||
|
||||
void altersingle(int alpha, int i, int b, int g, int r) {
|
||||
int *n;
|
||||
|
||||
// printf("New point %d: ", i);
|
||||
|
||||
n = network[i]; /* alter hit neuron */
|
||||
*n -= (alpha * (*n - b)) / initalpha;
|
||||
// printf("%f, ", *n / 16.0);
|
||||
n++;
|
||||
*n -= (alpha * (*n - g)) / initalpha;
|
||||
// printf("%f, ", *n / 16.0);
|
||||
n++;
|
||||
*n -= (alpha * (*n - r)) / initalpha;
|
||||
// printf("%f\n", *n / 16.0);
|
||||
}
|
||||
|
||||
|
||||
/* Move adjacent neurons by precomputed alpha*(1-((i-j)^2/[r]^2)) in radpower[|i-j|]
|
||||
--------------------------------------------------------------------------------- */
|
||||
|
||||
void alterneigh(int rad, int i, int b, int g, int r) {
|
||||
int j, k, lo, hi, a;
|
||||
int *p, *q;
|
||||
|
||||
lo = i - rad;
|
||||
if (lo < -1) lo = -1;
|
||||
hi = i + rad;
|
||||
if (hi > netsize) hi = netsize;
|
||||
|
||||
j = i + 1;
|
||||
k = i - 1;
|
||||
q = radpower;
|
||||
while ((j < hi) || (k > lo)) {
|
||||
a = (*(++q));
|
||||
if (j < hi) {
|
||||
// printf("New point %d: ", j);
|
||||
p = network[j];
|
||||
*p -= (a * (*p - b)) / alpharadbias;
|
||||
// printf("%f, ", *p / 16.0);
|
||||
p++;
|
||||
*p -= (a * (*p - g)) / alpharadbias;
|
||||
// printf("%f, ", *p / 16.0);
|
||||
p++;
|
||||
*p -= (a * (*p - r)) / alpharadbias;
|
||||
// printf("%f\n", *p / 16.0);
|
||||
j++;
|
||||
}
|
||||
if (k > lo) {
|
||||
// printf("New point %d: ", k);
|
||||
p = network[k];
|
||||
*p -= (a * (*p - b)) / alpharadbias;
|
||||
// printf("%f, ", *p / 16.0);
|
||||
p++;
|
||||
*p -= (a * (*p - g)) / alpharadbias;
|
||||
// printf("%f, ", *p / 16.0);
|
||||
p++;
|
||||
*p -= (a * (*p - r)) / alpharadbias;
|
||||
// printf("%f\n", *p / 16.0);
|
||||
k--;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/* Main Learning Loop
|
||||
------------------ */
|
||||
|
||||
void learn() {
|
||||
int i, j, b, g, r;
|
||||
int radius, rad, alpha, step, delta, samplepixels;
|
||||
const unsigned char *p;
|
||||
const unsigned char *lim;
|
||||
|
||||
alphadec = 30 + ((samplefac - 1) / 3);
|
||||
p = thepicture;
|
||||
lim = thepicture + lengthcount;
|
||||
samplepixels = lengthcount / (3 * samplefac);
|
||||
delta = samplepixels / ncycles;
|
||||
alpha = initalpha;
|
||||
radius = initradius;
|
||||
|
||||
rad = radius >> radiusbiasshift;
|
||||
if (rad <= 1) rad = 0;
|
||||
for (i = 0; i < rad; i++)
|
||||
radpower[i] = alpha * (((rad * rad - i * i) * radbias) / (rad * rad));
|
||||
|
||||
// fprintf(stderr,"beginning 1D learning: initial radius=%d\n", rad);
|
||||
|
||||
if ((lengthcount % prime1) != 0) step = 3 * prime1;
|
||||
else {
|
||||
if ((lengthcount % prime2) != 0) step = 3 * prime2;
|
||||
else {
|
||||
if ((lengthcount % prime3) != 0) step = 3 * prime3;
|
||||
else step = 3 * prime4;
|
||||
}
|
||||
}
|
||||
|
||||
i = 0;
|
||||
while (i < samplepixels) {
|
||||
b = p[0] << netbiasshift;
|
||||
g = p[1] << netbiasshift;
|
||||
r = p[2] << netbiasshift;
|
||||
j = contest(b, g, r);
|
||||
|
||||
altersingle(alpha, j, b, g, r);
|
||||
if (rad) alterneigh(rad, j, b, g, r); /* alter neighbours */
|
||||
|
||||
p += step;
|
||||
if (p >= lim) p -= lengthcount;
|
||||
|
||||
i++;
|
||||
if (i % delta == 0) {
|
||||
alpha -= alpha / alphadec;
|
||||
radius -= radius / radiusdec;
|
||||
rad = radius >> radiusbiasshift;
|
||||
if (rad <= 1) rad = 0;
|
||||
for (j = 0; j < rad; j++)
|
||||
radpower[j] = alpha * (((rad * rad - j * j) * radbias) / (rad * rad));
|
||||
}
|
||||
}
|
||||
// fprintf(stderr,"finished 1D learning: final alpha=%f !\n",((float)alpha)/initalpha);
|
||||
}
|
|
@ -1,83 +0,0 @@
|
|||
/* NeuQuant Neural-Net Quantization Algorithm Interface
|
||||
* ----------------------------------------------------
|
||||
*
|
||||
* Copyright (c) 1994 Anthony Dekker
|
||||
*
|
||||
* NEUQUANT Neural-Net quantization algorithm by Anthony Dekker, 1994.
|
||||
* See "Kohonen neural networks for optimal colour quantization"
|
||||
* in "Network: Computation in Neural Systems" Vol. 5 (1994) pp 351-367.
|
||||
* for a discussion of the algorithm.
|
||||
* See also http://members.ozemail.com.au/~dekker/NEUQUANT.HTML
|
||||
*
|
||||
* Any party obtaining a copy of these files from the author, directly or
|
||||
* indirectly, is granted, free of charge, a full and unrestricted irrevocable,
|
||||
* world-wide, paid up, royalty-free, nonexclusive right and license to deal
|
||||
* in this software and documentation files (the "Software"), including without
|
||||
* limitation the rights to use, copy, modify, merge, publish, distribute, sublicense,
|
||||
* and/or sell copies of the Software, and to permit persons who receive
|
||||
* copies from any such party to do so, with the only requirement being
|
||||
* that this copyright notice remain intact.
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <cstdio>
|
||||
#include <cstdint>
|
||||
|
||||
|
||||
#define netsize 256 /* number of colours used */
|
||||
|
||||
|
||||
/* For 256 colours, fixed arrays need 8kb, plus space for the image
|
||||
---------------------------------------------------------------- */
|
||||
|
||||
|
||||
/* four primes near 500 - assume no image has a length so large */
|
||||
/* that it is divisible by all four primes */
|
||||
#define prime1 499
|
||||
#define prime2 491
|
||||
#define prime3 487
|
||||
#define prime4 503
|
||||
|
||||
#define minpicturebytes (3*prime4) /* minimum size for input image */
|
||||
|
||||
int getNetwork(int i, int j);
|
||||
|
||||
/* Initialise network in range (0,0,0) to (255,255,255) and set parameters
|
||||
----------------------------------------------------------------------- */
|
||||
void initnet(const unsigned char *thepic, int len, int sample);
|
||||
|
||||
/* Unbias network to give byte values 0..255 and record position i to prepare for sort
|
||||
----------------------------------------------------------------------------------- */
|
||||
void unbiasnet(); /* can edit this function to do output of colour map */
|
||||
|
||||
/* Output colour map
|
||||
----------------- */
|
||||
void writecolourmap(FILE *f);
|
||||
|
||||
void getcolourmap(uint8_t *colorMap);
|
||||
|
||||
/* Insertion sort of network and building of netindex[0..255] (to do after unbias)
|
||||
------------------------------------------------------------------------------- */
|
||||
void inxbuild();
|
||||
|
||||
/* Search for BGR values 0..255 (after net is unbiased) and return colour index
|
||||
---------------------------------------------------------------------------- */
|
||||
int inxsearch(int b, int g, int r);
|
||||
|
||||
/* Main Learning Loop
|
||||
------------------ */
|
||||
void learn();
|
||||
|
||||
/* Program Skeleton
|
||||
----------------
|
||||
[select samplefac in range 1..30]
|
||||
pic = (unsigned char*) malloc(3*width*height);
|
||||
[read image from input file into pic]
|
||||
initnet(pic,3*width*height,samplefac);
|
||||
learn();
|
||||
unbiasnet();
|
||||
[write output image header, using writecolourmap(f),
|
||||
possibly editing the loops in that function]
|
||||
inxbuild();
|
||||
[write output image using inxsearch(b,g,r)] */
|
|
@ -0,0 +1,36 @@
|
|||
#include "gifencoder.h"
|
||||
#include <QImage>
|
||||
|
||||
GifEncoder::GifEncoder(QObject *parent) : QObject(parent) {}
|
||||
|
||||
bool GifEncoder::open(QString filename, int width, int height) {
|
||||
CGIFrgb_Config config;
|
||||
memset(&config, 0, sizeof(config));
|
||||
config.path = filename.toLocal8Bit().constData();
|
||||
config.width = uint16_t(width);
|
||||
config.height = uint16_t(height);
|
||||
pGIF = cgif_rgb_newgif(&config);
|
||||
return pGIF;
|
||||
}
|
||||
|
||||
bool GifEncoder::push(QImage &image, int delayTime) {
|
||||
if (pGIF == nullptr) {
|
||||
return false;
|
||||
}
|
||||
CGIFrgb_FrameConfig fconfig;
|
||||
memset(&fconfig, 0, sizeof(fconfig));
|
||||
fconfig.pImageData = reinterpret_cast<uint8_t *>(
|
||||
const_cast<unsigned char *>(image.constBits()));
|
||||
fconfig.fmtChan = CGIF_CHAN_FMT_RGBA;
|
||||
fconfig.delay = uint16_t(delayTime);
|
||||
cgif_rgb_addframe(pGIF, &fconfig);
|
||||
return true;
|
||||
}
|
||||
|
||||
bool GifEncoder::close() {
|
||||
if (cgif_rgb_close(pGIF) == CGIF_OK) {
|
||||
pGIF = nullptr;
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
|
@ -0,0 +1,21 @@
|
|||
#ifndef GIFENCODER_H
|
||||
#define GIFENCODER_H
|
||||
|
||||
#include "GifImage/encoder/inc/cgif.h"
|
||||
#include <QObject>
|
||||
|
||||
class GifEncoder : public QObject {
|
||||
Q_OBJECT
|
||||
public:
|
||||
explicit GifEncoder(QObject *parent = nullptr);
|
||||
|
||||
public slots:
|
||||
bool open(QString filename, int width, int height);
|
||||
bool push(QImage &image, int delayTime);
|
||||
bool close();
|
||||
|
||||
private:
|
||||
CGIFrgb *pGIF = nullptr;
|
||||
};
|
||||
|
||||
#endif // GIFENCODER_H
|
|
@ -0,0 +1,111 @@
|
|||
#ifndef CGIF_H
|
||||
#define CGIF_H
|
||||
|
||||
#include <stdint.h>
|
||||
#include <stdio.h>
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
// flags to set the GIF/frame-attributes
|
||||
#define CGIF_ATTR_IS_ANIMATED (1uL << 1) // make an animated GIF (default is non-animated GIF)
|
||||
#define CGIF_ATTR_NO_GLOBAL_TABLE (1uL << 2) // disable global color table (global color table is default)
|
||||
#define CGIF_ATTR_HAS_TRANSPARENCY (1uL << 3) // first entry in color table contains transparency (alpha channel)
|
||||
#define CGIF_ATTR_NO_LOOP (1uL << 4) // don't loop a GIF animation: only play it one time.
|
||||
#define CGIF_FRAME_ATTR_USE_LOCAL_TABLE (1uL << 0) // use a local color table for a frame (local color table is not used by default)
|
||||
#define CGIF_FRAME_ATTR_HAS_ALPHA (1uL << 1) // alpha channel index provided by user (transIndex field)
|
||||
#define CGIF_FRAME_ATTR_HAS_SET_TRANS (1uL << 2) // transparency setting provided by user (transIndex field)
|
||||
#define CGIF_FRAME_ATTR_INTERLACED (1uL << 3) // encode frame interlaced (default is not interlaced)
|
||||
// flags to decrease GIF-size
|
||||
#define CGIF_FRAME_GEN_USE_TRANSPARENCY (1uL << 0) // use transparency optimization (setting pixels identical to previous frame transparent)
|
||||
#define CGIF_FRAME_GEN_USE_DIFF_WINDOW (1uL << 1) // do encoding just for the sub-window that has changed from previous frame
|
||||
|
||||
#define CGIF_INFINITE_LOOP (0x0000uL) // for animated GIF: 0 specifies infinite loop
|
||||
|
||||
typedef enum {
|
||||
CGIF_ERROR = -1, // something unspecified failed
|
||||
CGIF_OK = 0, // everything OK
|
||||
CGIF_EWRITE, // writing GIF data failed
|
||||
CGIF_EALLOC, // allocating memory failed
|
||||
CGIF_ECLOSE, // final call to fclose failed
|
||||
CGIF_EOPEN, // failed to open output file
|
||||
CGIF_EINDEX, // invalid index in image data provided by user
|
||||
// internal section (values subject to change)
|
||||
CGIF_PENDING,
|
||||
} cgif_result;
|
||||
|
||||
typedef enum {
|
||||
CGIF_CHAN_FMT_RGB = 3, // 3 byte per pixel (red, green, blue)
|
||||
CGIF_CHAN_FMT_RGBA = 4, // 4 byte per pixel (red, green, blue, alpha)
|
||||
} cgif_chan_fmt;
|
||||
|
||||
typedef struct st_gif CGIF; // struct for the full GIF
|
||||
typedef struct st_gifconfig CGIF_Config; // global cofinguration parameters of the GIF
|
||||
typedef struct st_frameconfig CGIF_FrameConfig; // local configuration parameters for a frame
|
||||
typedef struct st_cgif_rgb_config CGIFrgb_Config;
|
||||
typedef struct st_cgif_rgb CGIFrgb;
|
||||
typedef struct st_cgif_rgb_frameconfig CGIFrgb_FrameConfig;
|
||||
|
||||
typedef int cgif_write_fn(void* pContext, const uint8_t* pData, const size_t numBytes); // callback function for stream-based output
|
||||
|
||||
// prototypes
|
||||
CGIF* cgif_newgif (CGIF_Config* pConfig); // creates a new GIF (returns pointer to new GIF or NULL on error)
|
||||
int cgif_addframe (CGIF* pGIF, CGIF_FrameConfig* pConfig); // adds the next frame to an existing GIF (returns 0 on success)
|
||||
int cgif_close (CGIF* pGIF); // close file and free allocated memory (returns 0 on success)
|
||||
|
||||
CGIFrgb* cgif_rgb_newgif (const CGIFrgb_Config* pConfig);
|
||||
cgif_result cgif_rgb_addframe (CGIFrgb* pGIF, const CGIFrgb_FrameConfig* pConfig);
|
||||
cgif_result cgif_rgb_close (CGIFrgb* pGIF);
|
||||
|
||||
// CGIF_Config type (parameters passed by user)
|
||||
// note: must stay AS IS for backward compatibility
|
||||
struct st_gifconfig {
|
||||
uint8_t* pGlobalPalette; // global color table of the GIF
|
||||
const char* path; // path of the GIF to be created, mutually exclusive with pWriteFn
|
||||
uint32_t attrFlags; // fixed attributes of the GIF (e.g. whether it is animated or not)
|
||||
uint32_t genFlags; // flags that determine how the GIF is generated (e.g. optimization)
|
||||
uint16_t width; // width of each frame in the GIF
|
||||
uint16_t height; // height of each frame in the GIF
|
||||
uint16_t numGlobalPaletteEntries; // size of the global color table
|
||||
uint16_t numLoops; // number of repetitons of an animated GIF (set to INFINITE_LOOP for infinite loop)
|
||||
cgif_write_fn *pWriteFn; // callback function for chunks of output data, mutually exclusive with path
|
||||
void* pContext; // opaque pointer passed as the first parameter to pWriteFn
|
||||
};
|
||||
|
||||
// CGIF_FrameConfig type (parameters passed by user)
|
||||
// note: must stay AS IS for backward compatibility
|
||||
struct st_frameconfig {
|
||||
uint8_t* pLocalPalette; // local color table of a frame
|
||||
uint8_t* pImageData; // image data to be encoded
|
||||
uint32_t attrFlags; // fixed attributes of the GIF frame
|
||||
uint32_t genFlags; // flags that determine how the GIF frame is created (e.g. optimization)
|
||||
uint16_t delay; // delay before the next frame is shown (units of 0.01 s)
|
||||
uint16_t numLocalPaletteEntries; // size of the local color table
|
||||
uint8_t transIndex; // introduced with V0.2.0
|
||||
};
|
||||
|
||||
struct st_cgif_rgb_config {
|
||||
cgif_write_fn* pWriteFn;
|
||||
void* pContext;
|
||||
const char* path;
|
||||
uint32_t attrFlags;
|
||||
uint32_t genFlags;
|
||||
uint16_t numLoops;
|
||||
uint16_t width;
|
||||
uint16_t height;
|
||||
};
|
||||
|
||||
struct st_cgif_rgb_frameconfig {
|
||||
uint8_t* pImageData;
|
||||
cgif_chan_fmt fmtChan;
|
||||
uint32_t attrFlags; // TBD
|
||||
uint32_t genFlags; // TBD
|
||||
uint16_t delay;
|
||||
};
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
|
||||
#endif // CGIF_H
|
|
@ -0,0 +1,69 @@
|
|||
#ifndef CGIF_RAW_H
|
||||
#define CGIF_RAW_H
|
||||
|
||||
#include <stdint.h>
|
||||
|
||||
#include "cgif.h"
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
#define DISPOSAL_METHOD_LEAVE (1uL << 2)
|
||||
#define DISPOSAL_METHOD_BACKGROUND (2uL << 2)
|
||||
#define DISPOSAL_METHOD_PREVIOUS (3uL << 2)
|
||||
|
||||
// flags to set the GIF attributes
|
||||
#define CGIF_RAW_ATTR_IS_ANIMATED (1uL << 0) // make an animated GIF (default is non-animated GIF)
|
||||
#define CGIF_RAW_ATTR_NO_LOOP (1uL << 1) // don't loop a GIF animation: only play it one time.
|
||||
|
||||
// flags to set the Frame attributes
|
||||
#define CGIF_RAW_FRAME_ATTR_HAS_TRANS (1uL << 0) // provided transIndex should be set
|
||||
#define CGIF_RAW_FRAME_ATTR_INTERLACED (1uL << 1) // encode frame interlaced
|
||||
|
||||
// CGIFRaw_Config type
|
||||
// note: internal sections, subject to change.
|
||||
typedef struct {
|
||||
cgif_write_fn *pWriteFn; // callback function for chunks of output data
|
||||
void* pContext; // opaque pointer passed as the first parameter to pWriteFn
|
||||
uint8_t* pGCT; // global color table of the GIF
|
||||
uint32_t attrFlags; // fixed attributes of the GIF (e.g. whether it is animated or not)
|
||||
uint16_t width; // effective width of each frame in the GIF
|
||||
uint16_t height; // effective height of each frame in the GIF
|
||||
uint16_t sizeGCT; // size of the global color table (GCT)
|
||||
uint16_t numLoops; // number of repetitons of an animated GIF (set to INFINITE_LOOP resp. 0 for infinite loop, use CGIF_ATTR_NO_LOOP if you don't want any repetition)
|
||||
} CGIFRaw_Config;
|
||||
|
||||
// CGIFRaw_FrameConfig type
|
||||
// note: internal sections, subject to chage.
|
||||
typedef struct {
|
||||
uint8_t* pLCT; // local color table of the frame (LCT)
|
||||
uint8_t* pImageData; // image data to be encoded (indices to CT)
|
||||
uint32_t attrFlags; // fixed attributes of the GIF frame
|
||||
uint16_t width; // width of frame
|
||||
uint16_t height; // height of frame
|
||||
uint16_t top; // top offset of frame
|
||||
uint16_t left; // left offset of frame
|
||||
uint16_t delay; // delay before the next frame is shown (units of 0.01 s [cs])
|
||||
uint16_t sizeLCT; // size of the local color table (LCT)
|
||||
uint8_t disposalMethod; // specifies how this frame should be disposed after being displayed.
|
||||
uint8_t transIndex; // transparency index
|
||||
} CGIFRaw_FrameConfig;
|
||||
|
||||
// CGIFRaw type
|
||||
// note: internal sections, subject to change.
|
||||
typedef struct {
|
||||
CGIFRaw_Config config; // configutation parameters of the GIF (see above)
|
||||
cgif_result curResult; // current result status of GIFRaw stream
|
||||
} CGIFRaw;
|
||||
|
||||
// prototypes
|
||||
CGIFRaw* cgif_raw_newgif (const CGIFRaw_Config* pConfig);
|
||||
cgif_result cgif_raw_addframe (CGIFRaw* pGIF, const CGIFRaw_FrameConfig* pConfig);
|
||||
cgif_result cgif_raw_close (CGIFRaw* pGIF);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
|
||||
#endif // CGIF_RAW_H
|
|
@ -0,0 +1,494 @@
|
|||
#include <stdint.h>
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
#include <stdlib.h>
|
||||
|
||||
#include "cgif.h"
|
||||
#include "cgif_raw.h"
|
||||
|
||||
#define MULU16(a, b) (((uint32_t)a) * ((uint32_t)b)) // helper macro to correctly multiply two U16's without default signed int promotion
|
||||
#define SIZE_FRAME_QUEUE (3)
|
||||
|
||||
// CGIF_Frame type
|
||||
// note: internal sections, subject to change in future versions
|
||||
typedef struct {
|
||||
CGIF_FrameConfig config;
|
||||
uint8_t disposalMethod;
|
||||
uint8_t transIndex;
|
||||
} CGIF_Frame;
|
||||
|
||||
// CGIF type
|
||||
// note: internal sections, subject to change in future versions
|
||||
struct st_gif {
|
||||
CGIF_Frame* aFrames[SIZE_FRAME_QUEUE]; // (internal) we need to keep the last three frames in memory.
|
||||
CGIF_Config config; // (internal) configuration parameters of the GIF
|
||||
CGIFRaw* pGIFRaw; // (internal) raw GIF stream
|
||||
FILE* pFile;
|
||||
cgif_result curResult;
|
||||
};
|
||||
|
||||
// dimension result type
|
||||
typedef struct {
|
||||
uint16_t width;
|
||||
uint16_t height;
|
||||
uint16_t top;
|
||||
uint16_t left;
|
||||
} DimResult;
|
||||
|
||||
/* calculate next power of two exponent of given number (n MUST be <= 256) */
|
||||
static uint8_t calcNextPower2Ex(uint16_t n) {
|
||||
uint8_t nextPow2;
|
||||
|
||||
for (nextPow2 = 0; n > (1uL << nextPow2); ++nextPow2);
|
||||
return nextPow2;
|
||||
}
|
||||
|
||||
/* write callback. returns 0 on success or -1 on error. */
|
||||
static int writecb(void* pContext, const uint8_t* pData, const size_t numBytes) {
|
||||
CGIF* pGIF;
|
||||
size_t r;
|
||||
|
||||
pGIF = (CGIF*)pContext;
|
||||
if(pGIF->pFile) {
|
||||
r = fwrite(pData, 1, numBytes, pGIF->pFile);
|
||||
if(r == numBytes) return 0;
|
||||
else return -1;
|
||||
} else if(pGIF->config.pWriteFn) {
|
||||
return pGIF->config.pWriteFn(pGIF->config.pContext, pData, numBytes);
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* free space allocated for CGIF struct */
|
||||
static void freeCGIF(CGIF* pGIF) {
|
||||
if((pGIF->config.attrFlags & CGIF_ATTR_NO_GLOBAL_TABLE) == 0) {
|
||||
free(pGIF->config.pGlobalPalette);
|
||||
}
|
||||
free(pGIF);
|
||||
}
|
||||
|
||||
/* create a new GIF */
|
||||
CGIF* cgif_newgif(CGIF_Config* pConfig) {
|
||||
FILE* pFile;
|
||||
CGIF* pGIF;
|
||||
CGIFRaw* pGIFRaw; // raw GIF stream
|
||||
CGIFRaw_Config rawConfig = {0};
|
||||
|
||||
pFile = NULL;
|
||||
// open output file (if necessary)
|
||||
if(pConfig->path) {
|
||||
pFile = fopen(pConfig->path, "wb");
|
||||
if(pFile == NULL) {
|
||||
return NULL; // error: fopen failed
|
||||
}
|
||||
}
|
||||
// allocate space for CGIF context
|
||||
pGIF = malloc(sizeof(CGIF));
|
||||
if(pGIF == NULL) {
|
||||
if(pFile) {
|
||||
fclose(pFile);
|
||||
}
|
||||
return NULL; // error -> malloc failed
|
||||
}
|
||||
|
||||
memset(pGIF, 0, sizeof(CGIF));
|
||||
pGIF->pFile = pFile;
|
||||
memcpy(&(pGIF->config), pConfig, sizeof(CGIF_Config));
|
||||
// make a deep copy of global color tabele (GCT), if required.
|
||||
if((pConfig->attrFlags & CGIF_ATTR_NO_GLOBAL_TABLE) == 0) {
|
||||
pGIF->config.pGlobalPalette = malloc(pConfig->numGlobalPaletteEntries * 3);
|
||||
memcpy(pGIF->config.pGlobalPalette, pConfig->pGlobalPalette, pConfig->numGlobalPaletteEntries * 3);
|
||||
}
|
||||
|
||||
rawConfig.pGCT = pConfig->pGlobalPalette;
|
||||
rawConfig.sizeGCT = (pConfig->attrFlags & CGIF_ATTR_NO_GLOBAL_TABLE) ? 0 : pConfig->numGlobalPaletteEntries;
|
||||
// translate CGIF_ATTR_* to CGIF_RAW_ATTR_* flags
|
||||
rawConfig.attrFlags = (pConfig->attrFlags & CGIF_ATTR_IS_ANIMATED) ? CGIF_RAW_ATTR_IS_ANIMATED : 0;
|
||||
rawConfig.attrFlags |= (pConfig->attrFlags & CGIF_ATTR_NO_LOOP) ? CGIF_RAW_ATTR_NO_LOOP : 0;
|
||||
rawConfig.width = pConfig->width;
|
||||
rawConfig.height = pConfig->height;
|
||||
rawConfig.numLoops = pConfig->numLoops;
|
||||
rawConfig.pWriteFn = writecb;
|
||||
rawConfig.pContext = (void*)pGIF;
|
||||
// pass config down and create a new raw GIF stream.
|
||||
pGIFRaw = cgif_raw_newgif(&rawConfig);
|
||||
// check for errors
|
||||
if(pGIFRaw == NULL) {
|
||||
if(pFile) {
|
||||
fclose(pFile);
|
||||
}
|
||||
freeCGIF(pGIF);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
pGIF->pGIFRaw = pGIFRaw;
|
||||
// assume error per default.
|
||||
// set to CGIF_OK by the first successful cgif_addframe() call, as a GIF without frames is invalid.
|
||||
pGIF->curResult = CGIF_PENDING;
|
||||
return pGIF;
|
||||
}
|
||||
|
||||
/* compare given pixel indices using the correct local or global color table; returns 0 if the two pixels are RGB equal */
|
||||
static int cmpPixel(const CGIF* pGIF, const CGIF_FrameConfig* pCur, const CGIF_FrameConfig* pBef, const uint8_t iCur, const uint8_t iBef) {
|
||||
uint8_t* pBefCT; // color table to use for pBef
|
||||
uint8_t* pCurCT; // color table to use for pCur
|
||||
|
||||
if((pCur->attrFlags & CGIF_FRAME_ATTR_HAS_SET_TRANS) && iCur == pCur->transIndex) {
|
||||
return 0; // identical
|
||||
}
|
||||
if((pBef->attrFlags & CGIF_FRAME_ATTR_HAS_SET_TRANS) && iBef == pBef->transIndex) {
|
||||
return 1; // done: cannot compare
|
||||
}
|
||||
// safety bounds check
|
||||
const uint16_t sizeCTBef = (pBef->attrFlags & CGIF_FRAME_ATTR_USE_LOCAL_TABLE) ? pBef->numLocalPaletteEntries : pGIF->config.numGlobalPaletteEntries;
|
||||
const uint16_t sizeCTCur = (pCur->attrFlags & CGIF_FRAME_ATTR_USE_LOCAL_TABLE) ? pCur->numLocalPaletteEntries : pGIF->config.numGlobalPaletteEntries;
|
||||
if((iBef >= sizeCTBef) || (iCur >= sizeCTCur)) {
|
||||
return 1; // error: out-of-bounds - cannot compare
|
||||
}
|
||||
pBefCT = (pBef->attrFlags & CGIF_FRAME_ATTR_USE_LOCAL_TABLE) ? pBef->pLocalPalette : pGIF->config.pGlobalPalette; // local or global table used?
|
||||
pCurCT = (pCur->attrFlags & CGIF_FRAME_ATTR_USE_LOCAL_TABLE) ? pCur->pLocalPalette : pGIF->config.pGlobalPalette; // local or global table used?
|
||||
return memcmp(pBefCT + iBef * 3, pCurCT + iCur * 3, 3);
|
||||
}
|
||||
|
||||
/* optimize GIF file size by only redrawing the rectangular area that differs from previous frame */
|
||||
static uint8_t* doWidthHeightOptim(CGIF* pGIF, CGIF_FrameConfig* pCur, CGIF_FrameConfig* pBef, DimResult* pResult) {
|
||||
uint8_t* pNewImageData;
|
||||
const uint8_t* pCurImageData;
|
||||
const uint8_t* pBefImageData;
|
||||
uint16_t i, x;
|
||||
uint16_t newHeight, newWidth, newLeft, newTop;
|
||||
const uint16_t width = pGIF->config.width;
|
||||
const uint16_t height = pGIF->config.height;
|
||||
uint8_t iCur, iBef;
|
||||
|
||||
pCurImageData = pCur->pImageData;
|
||||
pBefImageData = pBef->pImageData;
|
||||
// find top
|
||||
i = 0;
|
||||
while(i < height) {
|
||||
for(int c = 0; c < width; ++c) {
|
||||
iCur = *(pCurImageData + MULU16(i, width) + c);
|
||||
iBef = *(pBefImageData + MULU16(i, width) + c);
|
||||
if(cmpPixel(pGIF, pCur, pBef, iCur, iBef) != 0) {
|
||||
goto FoundTop;
|
||||
}
|
||||
}
|
||||
++i;
|
||||
}
|
||||
FoundTop:
|
||||
if(i == height) { // need dummy pixel (frame is identical with one before)
|
||||
// TBD we might make it possible to merge identical frames in the future
|
||||
newWidth = 1;
|
||||
newHeight = 1;
|
||||
newLeft = 0;
|
||||
newTop = 0;
|
||||
goto Done;
|
||||
}
|
||||
newTop = i;
|
||||
|
||||
// find actual height
|
||||
i = height - 1;
|
||||
while(i > newTop) {
|
||||
for(int c = 0; c < width; ++c) {
|
||||
iCur = *(pCurImageData + MULU16(i, width) + c);
|
||||
iBef = *(pBefImageData + MULU16(i, width) + c);
|
||||
if(cmpPixel(pGIF, pCur, pBef, iCur, iBef) != 0) {
|
||||
goto FoundHeight;
|
||||
}
|
||||
}
|
||||
--i;
|
||||
}
|
||||
FoundHeight:
|
||||
newHeight = (i + 1) - newTop;
|
||||
|
||||
// find left
|
||||
i = newTop;
|
||||
x = 0;
|
||||
while(cmpPixel(pGIF, pCur, pBef, pCurImageData[MULU16(i, width) + x], pBefImageData[MULU16(i, width) + x]) == 0) {
|
||||
++i;
|
||||
if(i > (newTop + newHeight - 1)) {
|
||||
++x; //(x==width cannot happen as goto Done is triggered in the only possible case before)
|
||||
i = newTop;
|
||||
}
|
||||
}
|
||||
newLeft = x;
|
||||
|
||||
// find actual width
|
||||
i = newTop;
|
||||
x = width - 1;
|
||||
while(cmpPixel(pGIF, pCur, pBef, pCurImageData[MULU16(i, width) + x], pBefImageData[MULU16(i, width) + x]) == 0) {
|
||||
++i;
|
||||
if(i > (newTop + newHeight - 1)) {
|
||||
--x; //(x<newLeft cannot happen as goto Done is triggered in the only possible case before)
|
||||
i = newTop;
|
||||
}
|
||||
}
|
||||
newWidth = (x + 1) - newLeft;
|
||||
|
||||
Done:
|
||||
|
||||
// create new image data
|
||||
pNewImageData = malloc(MULU16(newWidth, newHeight)); // TBD check return value of malloc
|
||||
for (i = 0; i < newHeight; ++i) {
|
||||
memcpy(pNewImageData + MULU16(i, newWidth), pCurImageData + MULU16((i + newTop), width) + newLeft, newWidth);
|
||||
}
|
||||
|
||||
// set new width, height, top, left in DimResult struct
|
||||
pResult->width = newWidth;
|
||||
pResult->height = newHeight;
|
||||
pResult->top = newTop;
|
||||
pResult->left = newLeft;
|
||||
return pNewImageData;
|
||||
}
|
||||
|
||||
/* move frame down to the raw GIF API */
|
||||
static cgif_result flushFrame(CGIF* pGIF, CGIF_Frame* pCur, CGIF_Frame* pBef) {
|
||||
CGIFRaw_FrameConfig rawConfig;
|
||||
DimResult dimResult;
|
||||
uint8_t* pTmpImageData;
|
||||
uint8_t* pBefImageData;
|
||||
int isFirstFrame, useLCT, hasAlpha, hasSetTransp;
|
||||
uint16_t numPaletteEntries;
|
||||
uint16_t imageWidth, imageHeight, width, height, top, left;
|
||||
uint8_t transIndex, disposalMethod;
|
||||
cgif_result r;
|
||||
|
||||
imageWidth = pGIF->config.width;
|
||||
imageHeight = pGIF->config.height;
|
||||
isFirstFrame = (pBef == NULL) ? 1 : 0;
|
||||
useLCT = (pCur->config.attrFlags & CGIF_FRAME_ATTR_USE_LOCAL_TABLE) ? 1 : 0; // LCT stands for "local color table"
|
||||
hasAlpha = ((pGIF->config.attrFlags & CGIF_ATTR_HAS_TRANSPARENCY) || (pCur->config.attrFlags & CGIF_FRAME_ATTR_HAS_ALPHA)) ? 1 : 0;
|
||||
hasSetTransp = (pCur->config.attrFlags & CGIF_FRAME_ATTR_HAS_SET_TRANS) ? 1 : 0;
|
||||
disposalMethod = pCur->disposalMethod;
|
||||
transIndex = pCur->transIndex;
|
||||
// sanity check:
|
||||
// at least one valid CT needed (global or local)
|
||||
if(!useLCT && (pGIF->config.attrFlags & CGIF_ATTR_NO_GLOBAL_TABLE)) {
|
||||
return CGIF_ERROR; // invalid config
|
||||
}
|
||||
// deactivate impossible size optimizations
|
||||
// => in case alpha channel is used
|
||||
// CGIF_FRAME_GEN_USE_TRANSPARENCY and CGIF_FRAME_GEN_USE_DIFF_WINDOW are not possible
|
||||
if(isFirstFrame || hasAlpha) {
|
||||
pCur->config.genFlags &= ~(CGIF_FRAME_GEN_USE_TRANSPARENCY | CGIF_FRAME_GEN_USE_DIFF_WINDOW);
|
||||
}
|
||||
// transparency setting (which areas are identical to the frame before) provided by user:
|
||||
// CGIF_FRAME_GEN_USE_TRANSPARENCY not possible
|
||||
if(hasSetTransp) {
|
||||
pCur->config.genFlags &= ~(CGIF_FRAME_GEN_USE_TRANSPARENCY);
|
||||
}
|
||||
numPaletteEntries = (useLCT) ? pCur->config.numLocalPaletteEntries : pGIF->config.numGlobalPaletteEntries;
|
||||
// switch off transparency optimization if color table is full (no free spot for the transparent index), TBD: count used colors, adapt table
|
||||
if(numPaletteEntries == 256) {
|
||||
pCur->config.genFlags &= ~CGIF_FRAME_GEN_USE_TRANSPARENCY;
|
||||
}
|
||||
|
||||
// purge overlap of current frame and frame before (width - height optim), if required (CGIF_FRAME_GEN_USE_DIFF_WINDOW set)
|
||||
if(pCur->config.genFlags & CGIF_FRAME_GEN_USE_DIFF_WINDOW) {
|
||||
pTmpImageData = doWidthHeightOptim(pGIF, &pCur->config, &pBef->config, &dimResult);
|
||||
width = dimResult.width;
|
||||
height = dimResult.height;
|
||||
top = dimResult.top;
|
||||
left = dimResult.left;
|
||||
} else {
|
||||
pTmpImageData = NULL;
|
||||
width = imageWidth;
|
||||
height = imageHeight;
|
||||
top = 0;
|
||||
left = 0;
|
||||
}
|
||||
|
||||
// mark matching areas of the previous frame as transparent, if required (CGIF_FRAME_GEN_USE_TRANSPARENCY set)
|
||||
if(pCur->config.genFlags & CGIF_FRAME_GEN_USE_TRANSPARENCY) {
|
||||
// set transIndex to next free index
|
||||
int pow2 = calcNextPower2Ex(numPaletteEntries);
|
||||
pow2 = (pow2 < 2) ? 2 : pow2; // TBD keep transparency index behavior as in V0.1.0 (for now)
|
||||
transIndex = (1 << pow2) - 1;
|
||||
if(transIndex < numPaletteEntries) {
|
||||
transIndex = (1 << (pow2 + 1)) - 1;
|
||||
}
|
||||
if(pTmpImageData == NULL) {
|
||||
pTmpImageData = malloc(MULU16(imageWidth, imageHeight)); // TBD check return value of malloc
|
||||
memcpy(pTmpImageData, pCur->config.pImageData, MULU16(imageWidth, imageHeight));
|
||||
}
|
||||
pBefImageData = pBef->config.pImageData;
|
||||
for(int i = 0; i < height; ++i) {
|
||||
for(int x = 0; x < width; ++x) {
|
||||
if(cmpPixel(pGIF, &pCur->config, &pBef->config, pTmpImageData[MULU16(i, width) + x], pBefImageData[MULU16(top + i, imageWidth) + (left + x)]) == 0) {
|
||||
pTmpImageData[MULU16(i, width) + x] = transIndex;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// move frame down to GIF raw API
|
||||
rawConfig.pLCT = pCur->config.pLocalPalette;
|
||||
rawConfig.pImageData = (pTmpImageData) ? pTmpImageData : pCur->config.pImageData;
|
||||
rawConfig.attrFlags = 0;
|
||||
if(hasAlpha || (pCur->config.genFlags & CGIF_FRAME_GEN_USE_TRANSPARENCY) || hasSetTransp) {
|
||||
rawConfig.attrFlags |= CGIF_RAW_FRAME_ATTR_HAS_TRANS;
|
||||
}
|
||||
rawConfig.attrFlags |= (pCur->config.attrFlags & CGIF_FRAME_ATTR_INTERLACED) ? CGIF_RAW_FRAME_ATTR_INTERLACED : 0;
|
||||
rawConfig.width = width;
|
||||
rawConfig.height = height;
|
||||
rawConfig.top = top;
|
||||
rawConfig.left = left;
|
||||
rawConfig.delay = pCur->config.delay;
|
||||
rawConfig.sizeLCT = (useLCT) ? pCur->config.numLocalPaletteEntries : 0;
|
||||
rawConfig.disposalMethod = disposalMethod;
|
||||
rawConfig.transIndex = transIndex;
|
||||
r = cgif_raw_addframe(pGIF->pGIFRaw, &rawConfig);
|
||||
free(pTmpImageData);
|
||||
return r;
|
||||
}
|
||||
|
||||
static void freeFrame(CGIF_Frame* pFrame) {
|
||||
if(pFrame) {
|
||||
free(pFrame->config.pImageData);
|
||||
if(pFrame->config.attrFlags & CGIF_FRAME_ATTR_USE_LOCAL_TABLE) {
|
||||
free(pFrame->config.pLocalPalette);
|
||||
}
|
||||
free(pFrame);
|
||||
}
|
||||
}
|
||||
|
||||
static void copyFrameConfig(CGIF_FrameConfig* pDest, CGIF_FrameConfig* pSrc) {
|
||||
pDest->pLocalPalette = pSrc->pLocalPalette; // might need a deep copy
|
||||
pDest->pImageData = pSrc->pImageData; // might need a deep copy
|
||||
pDest->attrFlags = pSrc->attrFlags;
|
||||
pDest->genFlags = pSrc->genFlags;
|
||||
pDest->delay = pSrc->delay;
|
||||
pDest->numLocalPaletteEntries = pSrc->numLocalPaletteEntries;
|
||||
// copy transIndex if necessary (field added with V0.2.0; avoid binary incompatibility)
|
||||
if(pSrc->attrFlags & (CGIF_FRAME_ATTR_HAS_ALPHA | CGIF_FRAME_ATTR_HAS_SET_TRANS)) {
|
||||
pDest->transIndex = pSrc->transIndex;
|
||||
}
|
||||
}
|
||||
|
||||
/* queue a new GIF frame */
|
||||
int cgif_addframe(CGIF* pGIF, CGIF_FrameConfig* pConfig) {
|
||||
CGIF_Frame* pNewFrame;
|
||||
int hasAlpha, hasSetTransp;
|
||||
int i;
|
||||
cgif_result r;
|
||||
|
||||
// check for previous errors
|
||||
if(pGIF->curResult != CGIF_OK && pGIF->curResult != CGIF_PENDING) {
|
||||
return pGIF->curResult;
|
||||
}
|
||||
hasAlpha = ((pGIF->config.attrFlags & CGIF_ATTR_HAS_TRANSPARENCY) || (pConfig->attrFlags & CGIF_FRAME_ATTR_HAS_ALPHA)) ? 1 : 0; // alpha channel is present
|
||||
hasSetTransp = (pConfig->attrFlags & CGIF_FRAME_ATTR_HAS_SET_TRANS) ? 1 : 0; // user provided transparency setting (identical areas marked by user)
|
||||
// check for invalid configs:
|
||||
// cannot set alpha channel and user-provided transparency at the same time.
|
||||
if(hasAlpha && hasSetTransp) {
|
||||
pGIF->curResult = CGIF_ERROR;
|
||||
return pGIF->curResult;
|
||||
}
|
||||
// cannot set global and local alpha channel at the same time
|
||||
if((pGIF->config.attrFlags & CGIF_ATTR_HAS_TRANSPARENCY) && (pConfig->attrFlags & CGIF_FRAME_ATTR_HAS_ALPHA)) {
|
||||
pGIF->curResult = CGIF_ERROR;
|
||||
return pGIF->curResult;
|
||||
}
|
||||
// search for free slot in frame queue
|
||||
for(i = 1; i < SIZE_FRAME_QUEUE && pGIF->aFrames[i] != NULL; ++i);
|
||||
// check whether the queue is full
|
||||
// when queue is full: we need to flush one frame.
|
||||
if(i == SIZE_FRAME_QUEUE) {
|
||||
r = flushFrame(pGIF, pGIF->aFrames[1], pGIF->aFrames[0]);
|
||||
freeFrame(pGIF->aFrames[0]);
|
||||
pGIF->aFrames[0] = NULL;
|
||||
// check for errors
|
||||
if(r != CGIF_OK) {
|
||||
pGIF->curResult = r;
|
||||
return pGIF->curResult;
|
||||
}
|
||||
i = SIZE_FRAME_QUEUE - 1;
|
||||
// keep the flushed frame in memory, as we might need it to write the next one.
|
||||
pGIF->aFrames[0] = pGIF->aFrames[1];
|
||||
pGIF->aFrames[1] = pGIF->aFrames[2];
|
||||
}
|
||||
// create new Frame struct + make a deep copy of pConfig.
|
||||
pNewFrame = malloc(sizeof(CGIF_Frame));
|
||||
copyFrameConfig(&(pNewFrame->config), pConfig);
|
||||
pNewFrame->config.pImageData = malloc(MULU16(pGIF->config.width, pGIF->config.height));
|
||||
memcpy(pNewFrame->config.pImageData, pConfig->pImageData, MULU16(pGIF->config.width, pGIF->config.height));
|
||||
// make a deep copy of the local color table, if required.
|
||||
if(pConfig->attrFlags & CGIF_FRAME_ATTR_USE_LOCAL_TABLE) {
|
||||
pNewFrame->config.pLocalPalette = malloc(pConfig->numLocalPaletteEntries * 3);
|
||||
memcpy(pNewFrame->config.pLocalPalette, pConfig->pLocalPalette, pConfig->numLocalPaletteEntries * 3);
|
||||
}
|
||||
pNewFrame->disposalMethod = DISPOSAL_METHOD_LEAVE;
|
||||
pNewFrame->transIndex = 0;
|
||||
pGIF->aFrames[i] = pNewFrame; // add frame to queue
|
||||
// check whether we need to adapt the disposal method of the frame before.
|
||||
if(pGIF->config.attrFlags & CGIF_ATTR_HAS_TRANSPARENCY) {
|
||||
pGIF->aFrames[i]->disposalMethod = DISPOSAL_METHOD_BACKGROUND; // TBD might be removed
|
||||
pGIF->aFrames[i]->transIndex = 0;
|
||||
if(pGIF->aFrames[i - 1] != NULL) {
|
||||
pGIF->aFrames[i - 1]->config.genFlags &= ~(CGIF_FRAME_GEN_USE_TRANSPARENCY | CGIF_FRAME_GEN_USE_DIFF_WINDOW);
|
||||
pGIF->aFrames[i - 1]->disposalMethod = DISPOSAL_METHOD_BACKGROUND; // restore to background color
|
||||
}
|
||||
}
|
||||
// set per-frame alpha channel (we need to adapt the disposal method of the frame before)
|
||||
if(pConfig->attrFlags & CGIF_FRAME_ATTR_HAS_ALPHA) {
|
||||
pGIF->aFrames[i]->transIndex = pConfig->transIndex;
|
||||
if(pGIF->aFrames[i - 1] != NULL) {
|
||||
pGIF->aFrames[i - 1]->config.genFlags &= ~(CGIF_FRAME_GEN_USE_DIFF_WINDOW); // width/height optim not possible for frame before
|
||||
pGIF->aFrames[i - 1]->disposalMethod = DISPOSAL_METHOD_BACKGROUND; // restore to background color
|
||||
}
|
||||
}
|
||||
// user provided transparency setting
|
||||
if(hasSetTransp) {
|
||||
pGIF->aFrames[i]->transIndex = pConfig->transIndex;
|
||||
}
|
||||
pGIF->curResult = CGIF_OK;
|
||||
return pGIF->curResult;
|
||||
}
|
||||
|
||||
/* close the GIF-file and free allocated space */
|
||||
int cgif_close(CGIF* pGIF) {
|
||||
int r;
|
||||
cgif_result result;
|
||||
|
||||
// check for previous errors
|
||||
if(pGIF->curResult != CGIF_OK) {
|
||||
goto CGIF_CLOSE_Cleanup;
|
||||
}
|
||||
|
||||
// flush all remaining frames in queue
|
||||
for(int i = 1; i < SIZE_FRAME_QUEUE; ++i) {
|
||||
if(pGIF->aFrames[i] != NULL) {
|
||||
r = flushFrame(pGIF, pGIF->aFrames[i], pGIF->aFrames[i - 1]);
|
||||
if(r != CGIF_OK) {
|
||||
pGIF->curResult = r;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// cleanup
|
||||
CGIF_CLOSE_Cleanup:
|
||||
r = cgif_raw_close(pGIF->pGIFRaw); // close raw GIF stream
|
||||
// check for errors
|
||||
if(r != CGIF_OK) {
|
||||
pGIF->curResult = r;
|
||||
}
|
||||
|
||||
if(pGIF->pFile) {
|
||||
r = fclose(pGIF->pFile); // we are done at this point => close the file
|
||||
if(r) {
|
||||
pGIF->curResult = CGIF_ECLOSE; // error: fclose failed
|
||||
}
|
||||
}
|
||||
for(int i = 0; i < 3; ++i) {
|
||||
freeFrame(pGIF->aFrames[i]);
|
||||
}
|
||||
|
||||
result = pGIF->curResult;
|
||||
freeCGIF(pGIF);
|
||||
// catch internal value CGIF_PENDING
|
||||
if(result == CGIF_PENDING) {
|
||||
result = CGIF_ERROR;
|
||||
}
|
||||
return result; // return previous result
|
||||
}
|
|
@ -0,0 +1,622 @@
|
|||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
|
||||
#include "cgif_raw.h"
|
||||
|
||||
#define SIZE_MAIN_HEADER (13)
|
||||
#define SIZE_APP_EXT (19)
|
||||
#define SIZE_FRAME_HEADER (10)
|
||||
#define SIZE_GRAPHIC_EXT ( 8)
|
||||
|
||||
#define HEADER_OFFSET_SIGNATURE (0x00)
|
||||
#define HEADER_OFFSET_VERSION (0x03)
|
||||
#define HEADER_OFFSET_WIDTH (0x06)
|
||||
#define HEADER_OFFSET_HEIGHT (0x08)
|
||||
#define HEADER_OFFSET_PACKED_FIELD (0x0A)
|
||||
#define HEADER_OFFSET_BACKGROUND (0x0B)
|
||||
#define HEADER_OFFSET_MAP (0x0C)
|
||||
|
||||
#define IMAGE_OFFSET_LEFT (0x01)
|
||||
#define IMAGE_OFFSET_TOP (0x03)
|
||||
#define IMAGE_OFFSET_WIDTH (0x05)
|
||||
#define IMAGE_OFFSET_HEIGHT (0x07)
|
||||
#define IMAGE_OFFSET_PACKED_FIELD (0x09)
|
||||
|
||||
#define IMAGE_PACKED_FIELD(a) (*((uint8_t*) (a + IMAGE_OFFSET_PACKED_FIELD)))
|
||||
|
||||
#define APPEXT_OFFSET_NAME (0x03)
|
||||
#define APPEXT_NETSCAPE_OFFSET_LOOPS (APPEXT_OFFSET_NAME + 13)
|
||||
|
||||
#define GEXT_OFFSET_DELAY (0x04)
|
||||
|
||||
#define MAX_CODE_LEN 12 // maximum code length for lzw
|
||||
#define MAX_DICT_LEN (1uL << MAX_CODE_LEN) // maximum length of the dictionary
|
||||
#define BLOCK_SIZE 0xFF // number of bytes in one block of the image data
|
||||
|
||||
#define MULU16(a, b) (((uint32_t)a) * ((uint32_t)b)) // helper macro to correctly multiply two U16's without default signed int promotion
|
||||
|
||||
typedef struct {
|
||||
uint8_t* pRasterData;
|
||||
uint32_t sizeRasterData;
|
||||
} LZWResult;
|
||||
|
||||
typedef struct {
|
||||
uint16_t* pTreeInit; // LZW dictionary tree for the initial dictionary (0-255 max)
|
||||
uint16_t* pTreeList; // LZW dictionary tree as list (max. number of children per node = 1)
|
||||
uint16_t* pTreeMap; // LZW dictionary tree as map (backup to pTreeList in case more than 1 child is present)
|
||||
uint16_t* pLZWData; // pointer to LZW data
|
||||
const uint8_t* pImageData; // pointer to image data
|
||||
uint32_t numPixel; // number of pixels per frame
|
||||
uint32_t LZWPos; // position of the current LZW code
|
||||
uint16_t dictPos; // currrent position in dictionary, we need to store 0-4096 -- so there are at least 13 bits needed here
|
||||
uint16_t mapPos; // current position in LZW tree mapping table
|
||||
} LZWGenState;
|
||||
|
||||
/* converts host U16 to little-endian (LE) U16 */
|
||||
static uint16_t hU16toLE(const uint16_t n) {
|
||||
int isBE;
|
||||
uint16_t newVal;
|
||||
uint16_t one;
|
||||
|
||||
one = 1;
|
||||
isBE = *((uint8_t*)&one) ? 0 : 1;
|
||||
if(isBE) {
|
||||
newVal = (n >> 8) | (n << 8);
|
||||
} else {
|
||||
newVal = n; // already LE
|
||||
}
|
||||
return newVal;
|
||||
}
|
||||
|
||||
/* calculate next power of two exponent of given number (n MUST be <= 256) */
|
||||
static uint8_t calcNextPower2Ex(uint16_t n) {
|
||||
uint8_t nextPow2;
|
||||
|
||||
for (nextPow2 = 0; n > (1uL << nextPow2); ++nextPow2);
|
||||
return nextPow2;
|
||||
}
|
||||
|
||||
/* compute which initial LZW-code length is needed */
|
||||
static uint8_t calcInitCodeLen(uint16_t numEntries) {
|
||||
uint8_t index;
|
||||
|
||||
index = calcNextPower2Ex(numEntries);
|
||||
return (index < 3) ? 3 : index + 1;
|
||||
}
|
||||
|
||||
/* reset the dictionary of known LZW codes -- will reset the current code length as well */
|
||||
static void resetDict(LZWGenState* pContext, const uint16_t initDictLen) {
|
||||
pContext->dictPos = initDictLen + 2; // reset current position in dictionary (number of colors + 2 for start and end code)
|
||||
pContext->mapPos = 1;
|
||||
pContext->pLZWData[pContext->LZWPos] = initDictLen; // issue clear-code
|
||||
++(pContext->LZWPos); // increment position in LZW data
|
||||
// reset LZW list
|
||||
memset(pContext->pTreeInit, 0, initDictLen * sizeof(uint16_t) * initDictLen);
|
||||
memset(pContext->pTreeList, 0, ((sizeof(uint16_t) * 2) + sizeof(uint16_t)) * MAX_DICT_LEN);
|
||||
}
|
||||
|
||||
/* add new child node */
|
||||
static void add_child(LZWGenState* pContext, const uint16_t parentIndex, const uint16_t LZWIndex, const uint16_t initDictLen, const uint8_t nextColor) {
|
||||
uint16_t* pTreeList;
|
||||
uint16_t mapPos;
|
||||
|
||||
pTreeList = pContext->pTreeList;
|
||||
mapPos = pTreeList[parentIndex * (2 + 1)];
|
||||
if(!mapPos) { // if pTreeMap is not used yet for the parent node
|
||||
if(pTreeList[parentIndex * (2 + 1) + 2]) { // if at least one child node exists, switch to pTreeMap
|
||||
mapPos = pContext->mapPos;
|
||||
// add child to mapping table (pTreeMap)
|
||||
memset(pContext->pTreeMap + ((mapPos - 1) * initDictLen), 0, initDictLen * sizeof(uint16_t));
|
||||
pContext->pTreeMap[(mapPos - 1) * initDictLen + nextColor] = LZWIndex;
|
||||
pTreeList[parentIndex * (2 + 1)] = mapPos;
|
||||
++(pContext->mapPos);
|
||||
} else { // use the free spot in pTreeList for the child node
|
||||
pTreeList[parentIndex * (2 + 1) + 1] = nextColor; // color that leads to child node
|
||||
pTreeList[parentIndex * (2 + 1) + 2] = LZWIndex; // position of child node
|
||||
}
|
||||
} else { // directly add child node to pTreeMap
|
||||
pContext->pTreeMap[(mapPos - 1) * initDictLen + nextColor] = LZWIndex;
|
||||
}
|
||||
++(pContext->dictPos); // increase current position in the dictionary
|
||||
}
|
||||
|
||||
/* find next LZW code representing the longest pixel sequence that is still in the dictionary*/
|
||||
static int lzw_crawl_tree(LZWGenState* pContext, uint32_t* pStrPos, uint16_t parentIndex, const uint16_t initDictLen) {
|
||||
uint16_t* pTreeInit;
|
||||
uint16_t* pTreeList;
|
||||
uint32_t strPos;
|
||||
uint16_t nextParent;
|
||||
uint16_t mapPos;
|
||||
|
||||
if(parentIndex >= initDictLen) {
|
||||
return CGIF_EINDEX; // error: index in image data out-of-bounds
|
||||
}
|
||||
pTreeInit = pContext->pTreeInit;
|
||||
pTreeList = pContext->pTreeList;
|
||||
strPos = *pStrPos;
|
||||
// get the next LZW code from pTreeInit:
|
||||
// the initial nodes (0-255 max) have more children on average.
|
||||
// use the mapping approach right from the start for these nodes.
|
||||
if(strPos < (pContext->numPixel - 1)) {
|
||||
if(pContext->pImageData[strPos + 1] >= initDictLen) {
|
||||
return CGIF_EINDEX; // error: index in image data out-of-bounds
|
||||
}
|
||||
nextParent = pTreeInit[parentIndex * initDictLen + pContext->pImageData[strPos + 1]];
|
||||
if(nextParent) {
|
||||
parentIndex = nextParent;
|
||||
++strPos;
|
||||
} else {
|
||||
pContext->pLZWData[pContext->LZWPos] = parentIndex; // write last LZW code in LZW data
|
||||
++(pContext->LZWPos);
|
||||
if(pContext->dictPos < MAX_DICT_LEN) {
|
||||
pTreeInit[parentIndex * initDictLen + pContext->pImageData[strPos + 1]] = pContext->dictPos;
|
||||
++(pContext->dictPos);
|
||||
} else {
|
||||
resetDict(pContext, initDictLen);
|
||||
}
|
||||
++strPos;
|
||||
*pStrPos = strPos;
|
||||
return CGIF_OK;
|
||||
}
|
||||
}
|
||||
// inner loop for codes > initDictLen
|
||||
while(strPos < (pContext->numPixel - 1)) {
|
||||
if(pContext->pImageData[strPos + 1] >= initDictLen) {
|
||||
return CGIF_EINDEX; // error: index in image data out-of-bounds
|
||||
}
|
||||
// first try to find child in LZW list
|
||||
if(pTreeList[parentIndex * (2 + 1) + 2] && pTreeList[parentIndex * (2 + 1) + 1] == pContext->pImageData[strPos + 1]) {
|
||||
parentIndex = pTreeList[parentIndex * (2 + 1) + 2];
|
||||
++strPos;
|
||||
continue;
|
||||
}
|
||||
// not found child yet? try to look into the LZW mapping table
|
||||
mapPos = pContext->pTreeList[parentIndex * (2 + 1)];
|
||||
if(mapPos) {
|
||||
nextParent = pContext->pTreeMap[(mapPos - 1) * initDictLen + pContext->pImageData[strPos + 1]];
|
||||
if(nextParent) {
|
||||
parentIndex = nextParent;
|
||||
++strPos;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
// still not found child? add current parentIndex to LZW data and add new child
|
||||
pContext->pLZWData[pContext->LZWPos] = parentIndex; // write last LZW code in LZW data
|
||||
++(pContext->LZWPos);
|
||||
if(pContext->dictPos < MAX_DICT_LEN) { // if LZW-dictionary is not full yet
|
||||
add_child(pContext, parentIndex, pContext->dictPos, initDictLen, pContext->pImageData[strPos + 1]); // add new LZW code to dictionary
|
||||
} else {
|
||||
// the dictionary reached its maximum code => reset it (not required by GIF-standard but mostly done like this)
|
||||
resetDict(pContext, initDictLen);
|
||||
}
|
||||
++strPos;
|
||||
*pStrPos = strPos;
|
||||
return CGIF_OK;
|
||||
}
|
||||
pContext->pLZWData[pContext->LZWPos] = parentIndex; // if the end of the image is reached, write last LZW code
|
||||
++(pContext->LZWPos);
|
||||
++strPos;
|
||||
*pStrPos = strPos;
|
||||
return CGIF_OK;
|
||||
}
|
||||
|
||||
/* generate LZW-codes that compress the image data*/
|
||||
static int lzw_generate(LZWGenState* pContext, uint16_t initDictLen) {
|
||||
uint32_t strPos;
|
||||
int r;
|
||||
uint8_t parentIndex;
|
||||
|
||||
strPos = 0; // start at beginning of the image data
|
||||
resetDict(pContext, initDictLen); // reset dictionary and issue clear-code at first
|
||||
while(strPos < pContext->numPixel) { // while there are still image data to be encoded
|
||||
parentIndex = pContext->pImageData[strPos]; // start at root node
|
||||
// get longest sequence that is still in dictionary, return new position in image data
|
||||
r = lzw_crawl_tree(pContext, &strPos, (uint16_t)parentIndex, initDictLen);
|
||||
if(r != CGIF_OK) {
|
||||
return r; // error: return error code to callee
|
||||
}
|
||||
}
|
||||
pContext->pLZWData[pContext->LZWPos] = initDictLen + 1; // termination code
|
||||
++(pContext->LZWPos);
|
||||
return CGIF_OK;
|
||||
}
|
||||
|
||||
/* pack the LZW data into a byte sequence*/
|
||||
static uint32_t create_byte_list(uint8_t *byteList, uint32_t lzwPos, uint16_t *lzwStr, uint16_t initDictLen, uint8_t initCodeLen){
|
||||
uint32_t i;
|
||||
uint32_t dictPos; // counting new LZW codes
|
||||
uint16_t n = 2 * initDictLen; // if n - initDictLen == dictPos, the LZW code size is incremented by 1 bit
|
||||
uint32_t bytePos = 0; // position of current byte
|
||||
uint8_t bitOffset = 0; // number of bits used in the last byte
|
||||
uint8_t lzwCodeLen = initCodeLen; // dynamically increasing length of the LZW codes
|
||||
int correctLater = 0; // 1: one empty byte too much if end is reached after current code, 0 otherwise
|
||||
|
||||
byteList[0] = 0; // except from the 1st byte all other bytes should be initialized stepwise (below)
|
||||
// the very first symbol might be the clear-code. However, this is not mandatory. Quote:
|
||||
// "Encoders should output a Clear code as the first code of each image data stream."
|
||||
// We keep the option to NOT output the clear code as the first symbol in this function.
|
||||
dictPos = 1;
|
||||
for(i = 0; i < lzwPos; ++i) { // loop over all LZW codes
|
||||
if((lzwCodeLen < MAX_CODE_LEN) && ((uint32_t)(n - (initDictLen)) == dictPos)) { // larger code is used for the 1st time at i = 256 ...+ 512 ...+ 1024 -> 256, 768, 1792
|
||||
++lzwCodeLen; // increment the length of the LZW codes (bit units)
|
||||
n *= 2; // set threshold for next increment of LZW code size
|
||||
}
|
||||
correctLater = 0; // 1 indicates that one empty byte is too much at the end
|
||||
byteList[bytePos] |= ((uint8_t)(lzwStr[i] << bitOffset)); // add 1st bits of the new LZW code to the byte containing part of the previous code
|
||||
if(lzwCodeLen + bitOffset >= 8) { // if the current byte is not enough of the LZW code
|
||||
if(lzwCodeLen + bitOffset == 8) { // if just this byte is filled exactly
|
||||
byteList[++bytePos] = 0; // byte is full -- go to next byte and initialize as 0
|
||||
correctLater = 1; // use if one 0byte to much at the end
|
||||
} else if(lzwCodeLen + bitOffset < 16) { // if the next byte is not completely filled
|
||||
byteList[++bytePos] = (uint8_t)(lzwStr[i] >> (8-bitOffset));
|
||||
} else if(lzwCodeLen + bitOffset == 16) { // if the next byte is exactly filled by LZW code
|
||||
byteList[++bytePos] = (uint8_t)(lzwStr[i] >> (8-bitOffset));
|
||||
byteList[++bytePos] = 0; // byte is full -- go to next byte and initialize as 0
|
||||
correctLater = 1; // use if one 0byte to much at the end
|
||||
} else { // lzw-code ranges over 3 bytes in total
|
||||
byteList[++bytePos] = (uint8_t)(lzwStr[i] >> (8-bitOffset)); // write part of LZW code to next byte
|
||||
byteList[++bytePos] = (uint8_t)(lzwStr[i] >> (16-bitOffset)); // write part of LZW code to byte after next byte
|
||||
}
|
||||
}
|
||||
bitOffset = (lzwCodeLen + bitOffset) % 8; // how many bits of the last byte are used?
|
||||
++dictPos; // increment count of LZW codes
|
||||
if(lzwStr[i] == initDictLen) { // if a clear code appears in the LZW data
|
||||
lzwCodeLen = initCodeLen; // reset length of LZW codes
|
||||
n = 2 * initDictLen; // reset threshold for next increment of LZW code length
|
||||
dictPos = 1; // reset (see comment below)
|
||||
// take first code already into account to increment lzwCodeLen exactly when the code length cannot represent the current maximum symbol.
|
||||
// Note: This is usually done implicitly, as the very first symbol is a clear-code itself.
|
||||
}
|
||||
}
|
||||
// comment: the last byte can be zero in the following case only:
|
||||
// terminate code has been written (initial dict length + 1), but current code size is larger so padding zero bits were added and extend into the next byte(s).
|
||||
if(correctLater) { // if an unneccessaray empty 0-byte was initialized at the end
|
||||
--bytePos; // don't consider the last empty byte
|
||||
}
|
||||
return bytePos;
|
||||
}
|
||||
|
||||
/* put byte sequence in blocks as required by GIF-format */
|
||||
static uint32_t create_byte_list_block(uint8_t *byteList, uint8_t *byteListBlock, const uint32_t numBytes) {
|
||||
uint32_t i;
|
||||
uint32_t numBlock = numBytes / BLOCK_SIZE; // number of byte blocks with length BLOCK_SIZE
|
||||
uint8_t numRest = numBytes % BLOCK_SIZE; // number of bytes in last block (if not completely full)
|
||||
|
||||
for(i = 0; i < numBlock; ++i) { // loop over all blocks
|
||||
byteListBlock[i * (BLOCK_SIZE+1)] = BLOCK_SIZE; // number of bytes in the following block
|
||||
memcpy(byteListBlock + 1+i*(BLOCK_SIZE+1), byteList + i*BLOCK_SIZE, BLOCK_SIZE); // copy block from byteList to byteListBlock
|
||||
}
|
||||
if(numRest>0) {
|
||||
byteListBlock[numBlock*(BLOCK_SIZE+1)] = numRest; // number of bytes in the following block
|
||||
memcpy(byteListBlock + 1+numBlock*(BLOCK_SIZE+1), byteList + numBlock*BLOCK_SIZE, numRest); // copy block from byteList to byteListBlock
|
||||
byteListBlock[1 + numBlock * (BLOCK_SIZE + 1) + numRest] = 0; // set 0 at end of frame
|
||||
return 1 + numBlock * (BLOCK_SIZE + 1) + numRest; // index of last entry in byteListBlock
|
||||
}
|
||||
// all LZW blocks in the frame have the same block size (BLOCK_SIZE), so there are no remaining bytes to be writen.
|
||||
byteListBlock[numBlock *(BLOCK_SIZE + 1)] = 0; // set 0 at end of frame
|
||||
return numBlock *(BLOCK_SIZE + 1); // index of last entry in byteListBlock
|
||||
}
|
||||
|
||||
/* create all LZW raster data in GIF-format */
|
||||
static int LZW_GenerateStream(LZWResult* pResult, const uint32_t numPixel, const uint8_t* pImageData, const uint16_t initDictLen, const uint8_t initCodeLen){
|
||||
LZWGenState* pContext;
|
||||
uint32_t lzwPos, bytePos;
|
||||
uint32_t bytePosBlock;
|
||||
int r;
|
||||
// TBD recycle LZW tree list and map (if possible) to decrease the number of allocs
|
||||
pContext = malloc(sizeof(LZWGenState)); // TBD check return value of malloc
|
||||
pContext->pTreeInit = malloc((initDictLen * sizeof(uint16_t)) * initDictLen); // TBD check return value of malloc
|
||||
pContext->pTreeList = malloc(((sizeof(uint16_t) * 2) + sizeof(uint16_t)) * MAX_DICT_LEN); // TBD check return value of malloc TBD check size
|
||||
pContext->pTreeMap = malloc(((MAX_DICT_LEN / 2) + 1) * (initDictLen * sizeof(uint16_t))); // TBD check return value of malloc
|
||||
pContext->numPixel = numPixel;
|
||||
pContext->pImageData = pImageData;
|
||||
pContext->pLZWData = malloc(sizeof(uint16_t) * (numPixel + 2)); // TBD check return value of malloc
|
||||
pContext->LZWPos = 0;
|
||||
|
||||
// actually generate the LZW sequence.
|
||||
r = lzw_generate(pContext, initDictLen);
|
||||
if(r != CGIF_OK) {
|
||||
goto LZWGENERATE_Cleanup;
|
||||
}
|
||||
lzwPos = pContext->LZWPos;
|
||||
|
||||
// pack the generated LZW data into blocks of 255 bytes
|
||||
uint8_t *byteList; // lzw-data packed in byte-list
|
||||
uint8_t *byteListBlock; // lzw-data packed in byte-list with 255-block structure
|
||||
uint64_t MaxByteListLen = MAX_CODE_LEN*lzwPos/8ul +2ul +1ul; // conservative upper bound
|
||||
uint64_t MaxByteListBlockLen = MAX_CODE_LEN*lzwPos*(BLOCK_SIZE+1ul)/8ul/BLOCK_SIZE +2ul +1ul +1ul; // conservative upper bound
|
||||
byteList = malloc(MaxByteListLen); // TBD check return value of malloc
|
||||
byteListBlock = malloc(MaxByteListBlockLen); // TBD check return value of malloc
|
||||
bytePos = create_byte_list(byteList,lzwPos, pContext->pLZWData, initDictLen, initCodeLen);
|
||||
bytePosBlock = create_byte_list_block(byteList, byteListBlock, bytePos+1);
|
||||
free(byteList);
|
||||
pResult->sizeRasterData = bytePosBlock + 1; // save
|
||||
pResult->pRasterData = byteListBlock;
|
||||
LZWGENERATE_Cleanup:
|
||||
free(pContext->pLZWData);
|
||||
free(pContext->pTreeInit);
|
||||
free(pContext->pTreeList);
|
||||
free(pContext->pTreeMap);
|
||||
free(pContext);
|
||||
return r;
|
||||
}
|
||||
|
||||
/* initialize the header of the GIF */
|
||||
static void initMainHeader(const CGIFRaw_Config* pConfig, uint8_t* pHeader) {
|
||||
uint16_t width, height;
|
||||
uint8_t pow2GlobalPalette;
|
||||
|
||||
width = pConfig->width;
|
||||
height = pConfig->height;
|
||||
|
||||
// set header to a clean state
|
||||
memset(pHeader, 0, SIZE_MAIN_HEADER);
|
||||
|
||||
// set Signature field to value "GIF"
|
||||
pHeader[HEADER_OFFSET_SIGNATURE] = 'G';
|
||||
pHeader[HEADER_OFFSET_SIGNATURE + 1] = 'I';
|
||||
pHeader[HEADER_OFFSET_SIGNATURE + 2] = 'F';
|
||||
|
||||
// set Version field to value "89a"
|
||||
pHeader[HEADER_OFFSET_VERSION] = '8';
|
||||
pHeader[HEADER_OFFSET_VERSION + 1] = '9';
|
||||
pHeader[HEADER_OFFSET_VERSION + 2] = 'a';
|
||||
|
||||
// set width of screen (LE ordering)
|
||||
const uint16_t widthLE = hU16toLE(width);
|
||||
memcpy(pHeader + HEADER_OFFSET_WIDTH, &widthLE, sizeof(uint16_t));
|
||||
|
||||
// set height of screen (LE ordering)
|
||||
const uint16_t heightLE = hU16toLE(height);
|
||||
memcpy(pHeader + HEADER_OFFSET_HEIGHT, &heightLE, sizeof(uint16_t));
|
||||
|
||||
// init packed field
|
||||
if(pConfig->sizeGCT) {
|
||||
pHeader[HEADER_OFFSET_PACKED_FIELD] = (1 << 7); // M = 1 (see GIF specc): global color table is present
|
||||
// calculate needed size of global color table (GCT).
|
||||
// MUST be a power of two.
|
||||
pow2GlobalPalette = calcNextPower2Ex(pConfig->sizeGCT);
|
||||
pow2GlobalPalette = (pow2GlobalPalette < 1) ? 1 : pow2GlobalPalette; // minimum size is 2^1
|
||||
pHeader[HEADER_OFFSET_PACKED_FIELD] |= ((pow2GlobalPalette - 1) << 0); // set size of GCT (0 - 7 in header + 1)
|
||||
}
|
||||
}
|
||||
|
||||
/* initialize NETSCAPE app extension block (needed for animation) */
|
||||
static void initAppExtBlock(uint8_t* pAppExt, uint16_t numLoops) {
|
||||
memset(pAppExt, 0, SIZE_APP_EXT);
|
||||
// set data
|
||||
pAppExt[0] = 0x21;
|
||||
pAppExt[1] = 0xFF; // start of block
|
||||
pAppExt[2] = 0x0B; // eleven bytes to follow
|
||||
|
||||
// write identifier for Netscape animation extension
|
||||
pAppExt[APPEXT_OFFSET_NAME] = 'N';
|
||||
pAppExt[APPEXT_OFFSET_NAME + 1] = 'E';
|
||||
pAppExt[APPEXT_OFFSET_NAME + 2] = 'T';
|
||||
pAppExt[APPEXT_OFFSET_NAME + 3] = 'S';
|
||||
pAppExt[APPEXT_OFFSET_NAME + 4] = 'C';
|
||||
pAppExt[APPEXT_OFFSET_NAME + 5] = 'A';
|
||||
pAppExt[APPEXT_OFFSET_NAME + 6] = 'P';
|
||||
pAppExt[APPEXT_OFFSET_NAME + 7] = 'E';
|
||||
pAppExt[APPEXT_OFFSET_NAME + 8] = '2';
|
||||
pAppExt[APPEXT_OFFSET_NAME + 9] = '.';
|
||||
pAppExt[APPEXT_OFFSET_NAME + 10] = '0';
|
||||
pAppExt[APPEXT_OFFSET_NAME + 11] = 0x03; // 3 bytes to follow
|
||||
pAppExt[APPEXT_OFFSET_NAME + 12] = 0x01; // TBD clarify
|
||||
// set number of repetitions (animation; LE ordering)
|
||||
const uint16_t netscapeLE = hU16toLE(numLoops);
|
||||
memcpy(pAppExt + APPEXT_NETSCAPE_OFFSET_LOOPS, &netscapeLE, sizeof(uint16_t));
|
||||
}
|
||||
|
||||
/* write numBytes dummy bytes */
|
||||
static int writeDummyBytes(cgif_write_fn* pWriteFn, void* pContext, int numBytes) {
|
||||
int rWrite = 0;
|
||||
const uint8_t dummyByte = 0;
|
||||
|
||||
for(int i = 0; i < numBytes; ++i) {
|
||||
rWrite |= pWriteFn(pContext, &dummyByte, 1);
|
||||
}
|
||||
return rWrite;
|
||||
}
|
||||
|
||||
CGIFRaw* cgif_raw_newgif(const CGIFRaw_Config* pConfig) {
|
||||
uint8_t aAppExt[SIZE_APP_EXT];
|
||||
uint8_t aHeader[SIZE_MAIN_HEADER];
|
||||
CGIFRaw* pGIF;
|
||||
int rWrite;
|
||||
// check for invalid GCT size
|
||||
if(pConfig->sizeGCT > 256) {
|
||||
return NULL; // invalid GCT size
|
||||
}
|
||||
pGIF = malloc(sizeof(CGIFRaw));
|
||||
if(!pGIF) {
|
||||
return NULL;
|
||||
}
|
||||
memcpy(&(pGIF->config), pConfig, sizeof(CGIFRaw_Config));
|
||||
// initiate all sections we can at this stage:
|
||||
// - main GIF header
|
||||
// - global color table (GCT), if required
|
||||
// - netscape application extension (for animation), if required
|
||||
initMainHeader(pConfig, aHeader);
|
||||
rWrite = pConfig->pWriteFn(pConfig->pContext, aHeader, SIZE_MAIN_HEADER);
|
||||
|
||||
// GCT required? => write it.
|
||||
if(pConfig->sizeGCT) {
|
||||
rWrite |= pConfig->pWriteFn(pConfig->pContext, pConfig->pGCT, pConfig->sizeGCT * 3);
|
||||
uint8_t pow2GCT = calcNextPower2Ex(pConfig->sizeGCT);
|
||||
pow2GCT = (pow2GCT < 1) ? 1 : pow2GCT; // minimum size is 2^1
|
||||
const uint16_t numBytesLeft = ((1 << pow2GCT) - pConfig->sizeGCT) * 3;
|
||||
rWrite |= writeDummyBytes(pConfig->pWriteFn, pConfig->pContext, numBytesLeft);
|
||||
}
|
||||
// GIF should be animated? => init & write app extension header ("NETSCAPE2.0")
|
||||
// No loop? Don't write NETSCAPE extension.
|
||||
if((pConfig->attrFlags & CGIF_RAW_ATTR_IS_ANIMATED) && !(pConfig->attrFlags & CGIF_RAW_ATTR_NO_LOOP)) {
|
||||
initAppExtBlock(aAppExt, pConfig->numLoops);
|
||||
rWrite |= pConfig->pWriteFn(pConfig->pContext, aAppExt, SIZE_APP_EXT);
|
||||
}
|
||||
// check for write errors
|
||||
if(rWrite) {
|
||||
free(pGIF);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
// assume error per default.
|
||||
// set to CGIF_OK by the first successful cgif_raw_addframe() call, as a GIF without frames is invalid.
|
||||
pGIF->curResult = CGIF_PENDING;
|
||||
return pGIF;
|
||||
}
|
||||
|
||||
/* add new frame to the raw GIF stream */
|
||||
cgif_result cgif_raw_addframe(CGIFRaw* pGIF, const CGIFRaw_FrameConfig* pConfig) {
|
||||
uint8_t aFrameHeader[SIZE_FRAME_HEADER];
|
||||
uint8_t aGraphicExt[SIZE_GRAPHIC_EXT];
|
||||
LZWResult encResult;
|
||||
int r, rWrite;
|
||||
const int useLCT = pConfig->sizeLCT; // LCT stands for "local color table"
|
||||
const int isInterlaced = (pConfig->attrFlags & CGIF_RAW_FRAME_ATTR_INTERLACED) ? 1 : 0;
|
||||
uint16_t numEffColors; // number of effective colors
|
||||
uint16_t initDictLen;
|
||||
uint8_t pow2LCT, initCodeLen;
|
||||
|
||||
if(pGIF->curResult != CGIF_OK && pGIF->curResult != CGIF_PENDING) {
|
||||
return pGIF->curResult; // return previous error
|
||||
}
|
||||
// check for invalid LCT size
|
||||
if(pConfig->sizeLCT > 256) {
|
||||
pGIF->curResult = CGIF_ERROR; // invalid LCT size
|
||||
return pGIF->curResult;
|
||||
}
|
||||
|
||||
rWrite = 0;
|
||||
// set frame header to a clean state
|
||||
memset(aFrameHeader, 0, SIZE_FRAME_HEADER);
|
||||
// set needed fields in frame header
|
||||
aFrameHeader[0] = ','; // set frame seperator
|
||||
if(useLCT) {
|
||||
pow2LCT = calcNextPower2Ex(pConfig->sizeLCT);
|
||||
pow2LCT = (pow2LCT < 1) ? 1 : pow2LCT; // minimum size is 2^1
|
||||
IMAGE_PACKED_FIELD(aFrameHeader) = (1 << 7);
|
||||
// set size of local color table (0-7 in header + 1)
|
||||
IMAGE_PACKED_FIELD(aFrameHeader) |= ((pow2LCT- 1) << 0);
|
||||
numEffColors = pConfig->sizeLCT;
|
||||
} else {
|
||||
numEffColors = pGIF->config.sizeGCT; // global color table in use
|
||||
}
|
||||
// encode frame interlaced?
|
||||
IMAGE_PACKED_FIELD(aFrameHeader) |= (isInterlaced << 6);
|
||||
|
||||
// transparency in use? we might need to increase numEffColors
|
||||
if((pGIF->config.attrFlags & (CGIF_RAW_ATTR_IS_ANIMATED)) && (pConfig->attrFlags & (CGIF_RAW_FRAME_ATTR_HAS_TRANS)) && pConfig->transIndex >= numEffColors) {
|
||||
numEffColors = pConfig->transIndex + 1;
|
||||
}
|
||||
|
||||
// calculate initial code length and initial dict length
|
||||
initCodeLen = calcInitCodeLen(numEffColors);
|
||||
initDictLen = 1uL << (initCodeLen - 1);
|
||||
|
||||
const uint16_t frameWidthLE = hU16toLE(pConfig->width);
|
||||
const uint16_t frameHeightLE = hU16toLE(pConfig->height);
|
||||
const uint16_t frameTopLE = hU16toLE(pConfig->top);
|
||||
const uint16_t frameLeftLE = hU16toLE(pConfig->left);
|
||||
memcpy(aFrameHeader + IMAGE_OFFSET_WIDTH, &frameWidthLE, sizeof(uint16_t));
|
||||
memcpy(aFrameHeader + IMAGE_OFFSET_HEIGHT, &frameHeightLE, sizeof(uint16_t));
|
||||
memcpy(aFrameHeader + IMAGE_OFFSET_TOP, &frameTopLE, sizeof(uint16_t));
|
||||
memcpy(aFrameHeader + IMAGE_OFFSET_LEFT, &frameLeftLE, sizeof(uint16_t));
|
||||
// apply interlaced pattern
|
||||
// TBD creating a copy of pImageData is not ideal, but changes on the LZW encoding would
|
||||
// be necessary otherwise.
|
||||
if(isInterlaced) {
|
||||
uint8_t* pInterlaced = malloc(MULU16(pConfig->width, pConfig->height));
|
||||
if(pInterlaced == NULL) {
|
||||
pGIF->curResult = CGIF_EALLOC;
|
||||
return pGIF->curResult;
|
||||
}
|
||||
uint8_t* p = pInterlaced;
|
||||
// every 8th row (starting with row 0)
|
||||
for(uint32_t i = 0; i < pConfig->height; i += 8) {
|
||||
memcpy(p, pConfig->pImageData + i * pConfig->width, pConfig->width);
|
||||
p += pConfig->width;
|
||||
}
|
||||
// every 8th row (starting with row 4)
|
||||
for(uint32_t i = 4; i < pConfig->height; i += 8) {
|
||||
memcpy(p, pConfig->pImageData + i * pConfig->width, pConfig->width);
|
||||
p += pConfig->width;
|
||||
}
|
||||
// every 4th row (starting with row 2)
|
||||
for(uint32_t i = 2; i < pConfig->height; i += 4) {
|
||||
memcpy(p, pConfig->pImageData + i * pConfig->width, pConfig->width);
|
||||
p += pConfig->width;
|
||||
}
|
||||
// every 2th row (starting with row 1)
|
||||
for(uint32_t i = 1; i < pConfig->height; i += 2) {
|
||||
memcpy(p, pConfig->pImageData + i * pConfig->width, pConfig->width);
|
||||
p += pConfig->width;
|
||||
}
|
||||
r = LZW_GenerateStream(&encResult, MULU16(pConfig->width, pConfig->height), pInterlaced, initDictLen, initCodeLen);
|
||||
free(pInterlaced);
|
||||
} else {
|
||||
r = LZW_GenerateStream(&encResult, MULU16(pConfig->width, pConfig->height), pConfig->pImageData, initDictLen, initCodeLen);
|
||||
}
|
||||
|
||||
// generate LZW raster data (actual image data)
|
||||
// check for errors
|
||||
if(r != CGIF_OK) {
|
||||
pGIF->curResult = r;
|
||||
return r;
|
||||
}
|
||||
|
||||
// do things for animation / transparency, if required.
|
||||
if(pGIF->config.attrFlags & CGIF_RAW_ATTR_IS_ANIMATED) {
|
||||
memset(aGraphicExt, 0, SIZE_GRAPHIC_EXT);
|
||||
aGraphicExt[0] = 0x21;
|
||||
aGraphicExt[1] = 0xF9;
|
||||
aGraphicExt[2] = 0x04;
|
||||
aGraphicExt[3] = pConfig->disposalMethod;
|
||||
// set flag indicating that transparency is used, if required.
|
||||
if(pConfig->attrFlags & CGIF_RAW_FRAME_ATTR_HAS_TRANS) {
|
||||
aGraphicExt[3] |= 0x01;
|
||||
aGraphicExt[6] = pConfig->transIndex;
|
||||
}
|
||||
// set delay (LE ordering)
|
||||
const uint16_t delayLE = hU16toLE(pConfig->delay);
|
||||
memcpy(aGraphicExt + GEXT_OFFSET_DELAY, &delayLE, sizeof(uint16_t));
|
||||
}
|
||||
|
||||
// write frame
|
||||
const uint8_t initialCodeSize = initCodeLen - 1;
|
||||
if(pGIF->config.attrFlags & CGIF_RAW_ATTR_IS_ANIMATED) {
|
||||
rWrite |= pGIF->config.pWriteFn(pGIF->config.pContext, aGraphicExt, SIZE_GRAPHIC_EXT);
|
||||
}
|
||||
rWrite |= pGIF->config.pWriteFn(pGIF->config.pContext, aFrameHeader, SIZE_FRAME_HEADER);
|
||||
if(useLCT) {
|
||||
rWrite |= pGIF->config.pWriteFn(pGIF->config.pContext, pConfig->pLCT, pConfig->sizeLCT * 3);
|
||||
const uint16_t numBytesLeft = ((1 << pow2LCT) - pConfig->sizeLCT) * 3;
|
||||
rWrite |= writeDummyBytes(pGIF->config.pWriteFn, pGIF->config.pContext, numBytesLeft);
|
||||
}
|
||||
rWrite |= pGIF->config.pWriteFn(pGIF->config.pContext, &initialCodeSize, 1);
|
||||
rWrite |= pGIF->config.pWriteFn(pGIF->config.pContext, encResult.pRasterData, encResult.sizeRasterData);
|
||||
|
||||
// check for write errors
|
||||
if(rWrite) {
|
||||
pGIF->curResult = CGIF_EWRITE;
|
||||
} else {
|
||||
pGIF->curResult = CGIF_OK;
|
||||
}
|
||||
// cleanup
|
||||
free(encResult.pRasterData);
|
||||
return pGIF->curResult;
|
||||
}
|
||||
|
||||
cgif_result cgif_raw_close(CGIFRaw* pGIF) {
|
||||
int rWrite;
|
||||
cgif_result result;
|
||||
|
||||
rWrite = pGIF->config.pWriteFn(pGIF->config.pContext, (unsigned char*) ";", 1); // write term symbol
|
||||
// check for write errors
|
||||
if(rWrite) {
|
||||
pGIF->curResult = CGIF_EWRITE;
|
||||
}
|
||||
result = pGIF->curResult;
|
||||
free(pGIF);
|
||||
return result;
|
||||
}
|
|
@ -0,0 +1,541 @@
|
|||
#include <stdbool.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <math.h>
|
||||
|
||||
#include "cgif.h"
|
||||
|
||||
#define MULU16(a, b) (((uint32_t)a) * ((uint32_t)b)) // helper macro to correctly multiply two U16's without default signed int promotion
|
||||
#define MAX(a,b) (a >= b ? a : b)
|
||||
#define MIN(a,b) (a <= b ? a : b)
|
||||
#define MAX_TABLE_SIZE 16777216 // 256*256*256
|
||||
#define MAX_COLLISIONS 30 // maximum number of collisions before hash-table resize
|
||||
|
||||
struct st_cgif_rgb {
|
||||
CGIF* pGIF;
|
||||
CGIFrgb_Config config;
|
||||
uint8_t* pBefImageData;
|
||||
cgif_chan_fmt befFmtChan;
|
||||
};
|
||||
|
||||
typedef struct {
|
||||
uint8_t r,g,b,a;
|
||||
} Pixel;
|
||||
|
||||
typedef struct treeNode {
|
||||
struct treeNode* child0; // pointer to child-node (elements smaller or equal than mean)
|
||||
struct treeNode* child1; // pointer to child-node (elements larger than mean)
|
||||
float mean[3]; // average of the colors
|
||||
uint32_t idxMin, idxMax; // minimum and maximum index referring to global palette
|
||||
uint8_t cutDim; // dimension along which the cut (node split) is done
|
||||
uint8_t colIdx; // color index (only meaningful for leave node)
|
||||
bool isLeave; // is leave node or not
|
||||
} treeNode;
|
||||
|
||||
static uint64_t argmax64(float* arry, uint64_t n){
|
||||
uint64_t imax = 0;
|
||||
float vmax = 0;
|
||||
|
||||
for(uint64_t i = 0; i < n; ++i) {
|
||||
if(arry[i] > vmax){
|
||||
imax = i;
|
||||
vmax = arry[i];
|
||||
}
|
||||
}
|
||||
return imax;
|
||||
}
|
||||
|
||||
/* get the next prime number above the next power of two */
|
||||
static int getNextPrimePower2(int N){
|
||||
if(N <= 512) {return 521;}
|
||||
else if(N <= 1024) {return 1031;}
|
||||
else if (N <= 2048) {return 2053;}
|
||||
else if (N <= 4096) {return 4099;}
|
||||
else if (N <= 8192) {return 8209;}
|
||||
else if (N <= 16384) {return 16411;}
|
||||
else if (N <= 32768) {return 32771;}
|
||||
else if (N <= 65536) {return 65537;}
|
||||
else if (N <= 131072) {return 131101;}
|
||||
else if (N <= 262144) {return 262147;}
|
||||
else if (N <= 524288) {return 524309;}
|
||||
else if (N <= 1048576) {return 1048583;}
|
||||
else if (N <= 2097152) {return 2097169;}
|
||||
else if (N <= 4194304) {return 4194319;}
|
||||
else if (N <= 8388608) {return 8388617;}
|
||||
else {return MAX_TABLE_SIZE;} // largest table size that is meaningful (no prime number)
|
||||
}
|
||||
|
||||
static Pixel getPixelVal(const uint8_t* pData, cgif_chan_fmt fmtChan) {
|
||||
Pixel result;
|
||||
|
||||
result.r = *(pData++);
|
||||
result.g = *(pData++);
|
||||
result.b = *(pData++);
|
||||
// check whether there is a alpha channel, if so convert it to a 1 bit alpha channel.
|
||||
switch(fmtChan) {
|
||||
case CGIF_CHAN_FMT_RGB:
|
||||
result.a = 0xFF;
|
||||
break;
|
||||
case CGIF_CHAN_FMT_RGBA:
|
||||
result.a = (*(pData) <= 127) ? 0 : 0xFF;
|
||||
break;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/* get a hash from rgb color values (use open addressing, if entry does not exist, next free spot is returned) */
|
||||
static int32_t col_hash(const uint8_t* rgb, const uint8_t* hashTable, const uint8_t* indexUsed, const uint32_t tableSize, cgif_chan_fmt fmtChan, uint32_t* cntCollision) {
|
||||
uint32_t h;
|
||||
const Pixel pix = getPixelVal(rgb, fmtChan);
|
||||
*cntCollision = 0; // count the number of collisions
|
||||
// has alpha channel?
|
||||
if(pix.a == 0) {
|
||||
return -1;
|
||||
}
|
||||
h = (pix.r * 256 * 256 + pix.g * 256 + pix.b) % tableSize;
|
||||
while(indexUsed[h] != 0 && memcmp(rgb, &hashTable[3 * h], 3) != 0) { // until free spot is found
|
||||
++(*cntCollision);
|
||||
++h;
|
||||
h = h % tableSize; // start from beginning if there is no free spot at the end
|
||||
}
|
||||
return h;
|
||||
}
|
||||
|
||||
/* take array indexed by hash(rgb) and turn it into a dense array */
|
||||
static uint32_t* hash_to_dense(const uint8_t* pPalette, const uint32_t* arry, uint32_t cnt, const uint8_t* hashTable, const uint8_t* indexUsed, uint32_t tableSize, cgif_chan_fmt fmtChan) {
|
||||
// pPalette: color table with cnt entries
|
||||
// arry: array indexed by hash of RGB-value in pPalette
|
||||
uint32_t* arryDense = malloc(sizeof(uint32_t) * cnt);
|
||||
uint32_t h, cntCollision;
|
||||
|
||||
(void)fmtChan;
|
||||
for(uint32_t i = 0; i < cnt; ++i) {
|
||||
h = col_hash(&pPalette[3 * i], hashTable, indexUsed, tableSize, 3, &cntCollision);
|
||||
arryDense[i] = arry[h];
|
||||
}
|
||||
return arryDense;
|
||||
}
|
||||
|
||||
/* get mean of color-cloud along all 3 dimensions (at least one color must be present, otherwise div 0 issue)*/
|
||||
static void get_mean(const uint8_t* pPalette, const uint32_t* frequ, uint32_t idxMin, uint32_t idxMax, float* mean){
|
||||
// pPalette: RGB color palette
|
||||
// frequ: frequency of the colors (indexing as pPalette, no hashing)
|
||||
// idxMin, idxMax: palette range for which the mean is computed
|
||||
// mean: mean value along all three dimensions
|
||||
float m[3] = {0,0,0};
|
||||
float sum[3] = {0,0,0};
|
||||
uint32_t i;
|
||||
uint8_t dim;
|
||||
|
||||
for(i = idxMin; i <= idxMax; ++i) {
|
||||
for(dim = 0; dim < 3; ++dim) {
|
||||
sum[dim] += frequ[i];
|
||||
m[dim] += frequ[i] * pPalette[3 * i + dim];
|
||||
}
|
||||
}
|
||||
for(dim = 0; dim < 3; ++dim) {
|
||||
mean[dim] = m[dim] / sum[dim];
|
||||
}
|
||||
}
|
||||
|
||||
/* get variance of color-cloud along all 3 dimensions*/
|
||||
static void get_variance(const uint8_t* pPalette, const uint32_t* frequ, uint32_t idxMin, uint32_t idxMax, float* var, float* mean){
|
||||
// pPalette: RGB color palette
|
||||
// frequ: frequency of the colors (indexing as pPalette, no hashing)
|
||||
// idxMin, idxMax: palette range for which the variance is computed
|
||||
// var/mean: variance/mean value along all three dimensions (array with 3 entries)
|
||||
uint32_t i;
|
||||
uint8_t dim;
|
||||
float v[3] = {0,0,0};
|
||||
float sum[3] = {0,0,0};
|
||||
|
||||
get_mean(pPalette, frequ, idxMin, idxMax, mean);
|
||||
for(i = idxMin; i <= idxMax; ++i) {
|
||||
for(dim = 0; dim < 3; ++dim) {
|
||||
sum[dim] += frequ[i];
|
||||
v[dim] += frequ[i] * (pPalette[3 * i + dim] - mean[dim]) * (pPalette[3 * i + dim] - mean[dim]);
|
||||
}
|
||||
}
|
||||
for(dim = 0; dim < 3; ++dim) {
|
||||
var[dim] = v[dim] / sum[dim];
|
||||
}
|
||||
}
|
||||
|
||||
static treeNode* new_tree_node(uint8_t* pPalette, uint32_t* frequ, uint16_t* numLeaveNodes, uint32_t idxMin, uint32_t idxMax, uint8_t colIdx) {
|
||||
float var[3];
|
||||
|
||||
treeNode* node = malloc(sizeof(treeNode));
|
||||
node->idxMin = idxMin; // minimum color in pPalette belonging to the node
|
||||
node->idxMax = idxMax; // maximum color in pPalette belonging to the node
|
||||
get_variance(pPalette, frequ, idxMin, idxMax, var, node->mean);
|
||||
node->cutDim = argmax64(var, 3); // dimension along which the cut (node split) is done
|
||||
node->colIdx = colIdx; // index referring to (averaged) color in new color table
|
||||
node->isLeave = 1; // every new node starts as a leave node
|
||||
(*numLeaveNodes)++; // increase counter when leave node is added
|
||||
return node;
|
||||
}
|
||||
|
||||
/* create the decision tree. (Similar to qsort with limited depth: pPalette, frequ get sorted) */
|
||||
static void crawl_decision_tree(treeNode* root, uint16_t* numLeaveNodes, uint8_t* pPalette, uint32_t* frequ, uint16_t colMax) {
|
||||
uint32_t i, k, saveNum;
|
||||
uint16_t nodeIdx = 0;
|
||||
uint8_t saveBlk[3];
|
||||
treeNode* parent;
|
||||
treeNode* nodeList[512]; // array of pointers to nodes for breadth-first tree traversal
|
||||
nodeList[0] = root;
|
||||
|
||||
while(*numLeaveNodes <= (colMax - 1)){
|
||||
parent = nodeList[nodeIdx++]; // go to next node in the list
|
||||
if (parent->idxMax - parent->idxMin >= 1) { // condition for node split
|
||||
i = parent->idxMin; // start of block minimum
|
||||
k = parent->idxMax; // start at block maximum
|
||||
while(i < k) { // split parent node in two blocks (like one step in qsort)
|
||||
for(; pPalette[3 * i + parent->cutDim] <= parent->mean[parent->cutDim]; ++i); // && i<parent->idxMax not needed (other condition is false when i==parent>idxMax since there must be at most 1 element above mean)
|
||||
for(; pPalette[3 * k + parent->cutDim] > parent->mean[parent->cutDim]; --k); // && k>parent->idxMin not needed (other condition is false when k==parent>idxMin since there must be at most 1 element below mean)
|
||||
if(k > i) {
|
||||
memcpy(saveBlk, &(pPalette[3 * i]), 3);
|
||||
memcpy(&(pPalette[3 * i]), &(pPalette[3 * k]), 3); // swap RGB-blocks in pPalette
|
||||
memcpy(&(pPalette[3 * k]), saveBlk, 3); // swap RGB-blocks in pPalette
|
||||
saveNum = frequ[k];
|
||||
frequ[k] = frequ[i]; // swap also the frequency
|
||||
frequ[i] = saveNum; // sawp also the frequency
|
||||
}
|
||||
}
|
||||
parent->isLeave = 0; // parent is no leave node anymore when children added
|
||||
(*numLeaveNodes)--; // decrease counter since parent is removed as a leave node
|
||||
parent->child0 = new_tree_node(pPalette, frequ, numLeaveNodes, parent->idxMin, i - 1, parent->colIdx); // i-1 is last index of 1st block, one child takes color index from parent
|
||||
parent->child1 = new_tree_node(pPalette, frequ, numLeaveNodes, i, parent->idxMax, *numLeaveNodes);
|
||||
nodeList[2*(*numLeaveNodes) - 3] = parent->child0; // add new child nodes to the list (total number of nodes is always 2*(*numLeaveNodes)-1)
|
||||
nodeList[2*(*numLeaveNodes) - 2] = parent->child1; // add new child nodes to the list (total number of nodes is always 2*(*numLeaveNodes)-1)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* fill 256 color table using the decision tree */
|
||||
static void get_palette_from_decision_tree(const treeNode* root, uint8_t* pPalette256){
|
||||
if(root->isLeave == 0) {
|
||||
get_palette_from_decision_tree(root->child0, pPalette256);
|
||||
get_palette_from_decision_tree(root->child1, pPalette256);
|
||||
} else {
|
||||
pPalette256[3 * root->colIdx] = (uint8_t)roundf(root->mean[0]);
|
||||
pPalette256[3 * root->colIdx + 1] = (uint8_t)roundf(root->mean[1]);
|
||||
pPalette256[3 * root->colIdx + 2] = (uint8_t)roundf(root->mean[2]);
|
||||
}
|
||||
}
|
||||
|
||||
/* get color index by using the decision tree */
|
||||
static uint8_t get_leave_node_index(const treeNode* root, const float* rgb) {
|
||||
if(root->isLeave == 0) { // if there is a split
|
||||
if(rgb[root->cutDim] <= root->mean[root->cutDim]) {
|
||||
return get_leave_node_index(root->child0, rgb);
|
||||
} else {
|
||||
return get_leave_node_index(root->child1, rgb);
|
||||
}
|
||||
} else {
|
||||
return root->colIdx; // return color index of leave node
|
||||
}
|
||||
}
|
||||
|
||||
/* color quantization with mean cut method (TBD? switch to median cut)
|
||||
(works with dense palette, no hash table) */
|
||||
static treeNode* create_decision_tree(uint8_t* pPalette, uint32_t* pFrequDense, uint8_t* pPalette256, uint32_t cnt, uint16_t colMax, uint8_t depthMax){
|
||||
uint16_t numLeaveNodes = 0;
|
||||
treeNode* root = new_tree_node(pPalette, pFrequDense, &numLeaveNodes, 0, cnt - 1, 0);
|
||||
crawl_decision_tree(root, &numLeaveNodes, pPalette, pFrequDense, colMax);
|
||||
get_palette_from_decision_tree(root, pPalette256); // fill the reduced color table
|
||||
return root;
|
||||
}
|
||||
|
||||
/* free memory allocated for the tree */
|
||||
static void free_decision_tree(treeNode* root){
|
||||
if(root->isLeave == 0) { // if the node has children
|
||||
free_decision_tree(root->child0);
|
||||
free_decision_tree(root->child1);
|
||||
}
|
||||
free(root); // free root once free is called for children
|
||||
}
|
||||
|
||||
/* get image with max 256 color indices using Floyd-Steinberg dithering */
|
||||
static void get_quantized_dithered_image(uint8_t* pImageData, float* pImageDataRGBfloat, uint8_t* pPalette256, treeNode* root, uint32_t numPixel, uint32_t width, uint8_t dithering, uint8_t transIndex, cgif_chan_fmt fmtChan, uint8_t* pBef, cgif_chan_fmt befFmtChan, int hasAlpha) {
|
||||
// pImageData: image with (max 256) color indices (length: numPixel)
|
||||
// pImageDataRGBfloat: image with RGB colors (length: 3*numPixel) must be signed to avoid overflow due to error passing, float only needed because of 0.9 factor
|
||||
// pPalette256: quantized color palette (indexed by node->colIdx), only used if dithering is on
|
||||
// root: root node of the decision tree for color quantization
|
||||
// numPixel, width: size of the image
|
||||
// dithering: 0 (no dithering), 1: Floyd-Steinberg dithering, else: Sierra dithering
|
||||
uint32_t i;
|
||||
uint8_t k;
|
||||
int err; // color errors
|
||||
/*
|
||||
we noticed certain artifacts with Floyd-Steinberg dithering if the error is passed too far.
|
||||
use a damping factor to reduce these effects.
|
||||
*/
|
||||
const float factor = 0.90;
|
||||
if(!dithering) {
|
||||
for(i = 0; i < numPixel; ++i) {
|
||||
if(hasAlpha) {
|
||||
if(pImageDataRGBfloat[fmtChan * i + 3] == 0) {
|
||||
pImageData[i] = transIndex;
|
||||
continue;
|
||||
}
|
||||
} else {
|
||||
// do the transparency trick
|
||||
float* p = pImageDataRGBfloat;
|
||||
if(pBef && p[i * fmtChan] == pBef[i * befFmtChan] && p[i * fmtChan + 1] == pBef[i * befFmtChan + 1] && p[i * fmtChan + 2] == pBef[i * befFmtChan + 2]) {
|
||||
pImageData[i] = transIndex;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
pImageData[i] = get_leave_node_index(root, &pImageDataRGBfloat[fmtChan * i]); // use decision tree to get indices for new colors
|
||||
}
|
||||
} else {
|
||||
for(i = 0; i < numPixel; ++i) {
|
||||
if(fmtChan == CGIF_CHAN_FMT_RGBA) {
|
||||
if(pImageDataRGBfloat[fmtChan * i + 3] == 0) {
|
||||
pImageData[i] = transIndex;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
// TBD add the transparency trick
|
||||
|
||||
// restrict color + error to 0-255 interval
|
||||
for(k = 0; k<3; ++k) {
|
||||
pImageDataRGBfloat[fmtChan * i + k] = MAX(0,MIN(pImageDataRGBfloat[fmtChan * i + k], 255)); // cut to 0-255 before
|
||||
}
|
||||
|
||||
pImageData[i] = get_leave_node_index(root, &pImageDataRGBfloat[fmtChan * i]); // use decision tree to get indices for new colors
|
||||
for(k = 0; k<3; ++k) {
|
||||
err = pImageDataRGBfloat[fmtChan * i + k] - pPalette256[3 * pImageData[i] + k]; // compute color error
|
||||
|
||||
//diffuse error with Floyd-Steinberg dithering.
|
||||
if(dithering == 1) {
|
||||
if(i % width < width-1){
|
||||
pImageDataRGBfloat[fmtChan * (i+1) + k] += factor * (7*err >> 4);
|
||||
}
|
||||
if(i < numPixel - width){
|
||||
pImageDataRGBfloat[fmtChan * (i+width) + k] += factor * (5*err >> 4);
|
||||
if(i % width > 0){
|
||||
pImageDataRGBfloat[fmtChan * (i+width-1) + k] += factor * (3*err >> 4);
|
||||
}
|
||||
if(i % width < width-1){
|
||||
pImageDataRGBfloat[fmtChan * (i+width+1) + k] += factor * (1*err >> 4);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Sierra dithering
|
||||
if(i % width < width-1){
|
||||
pImageDataRGBfloat[fmtChan * (i+1) + k] += factor * (5*err >> 5);
|
||||
if(i % width < width-2){
|
||||
pImageDataRGBfloat[fmtChan * (i+2) + k] += factor * (3*err >> 5);
|
||||
}
|
||||
}
|
||||
if(i < numPixel - width){
|
||||
pImageDataRGBfloat[fmtChan * (i+width) + k] += factor * (5*err >> 5);
|
||||
if(i % width > 0){
|
||||
pImageDataRGBfloat[fmtChan * (i+width-1) + k] += factor * (4*err >> 5);
|
||||
if(i % width > 1){
|
||||
pImageDataRGBfloat[fmtChan * (i+width-2) + k] += factor * (2*err >> 5);
|
||||
}
|
||||
}
|
||||
if(i % width < width-1){
|
||||
pImageDataRGBfloat[fmtChan * (i+width+1) + k] += factor * (4*err >> 5);
|
||||
if(i % width < width-2){
|
||||
pImageDataRGBfloat[fmtChan * (i+width+2) + k] += factor * (2*err >> 5);
|
||||
}
|
||||
}
|
||||
if(i < numPixel - 2*width){
|
||||
pImageDataRGBfloat[fmtChan * (i+2*width) + k] += factor * (3*err >> 5);
|
||||
if(i % width > 0){
|
||||
pImageDataRGBfloat[fmtChan * (i+2*width-1) + k] += factor * (2*err >> 5);
|
||||
}
|
||||
if(i % width < width-1){
|
||||
pImageDataRGBfloat[fmtChan * (i+2*width+1) + k] += factor * (2*err >> 5);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* convert rgb image to indices and palette
|
||||
The overall number of colors must already be <= 256 which is e.g. the case when the image comes directly from a GIF */
|
||||
// pImageDataRGB: image in RGB (input)
|
||||
// numPixel,width:number of pixels of input image, width (only used for dithering)
|
||||
// fmtChan: select if image data are RGB or RGBa
|
||||
// pImageData: pointer to image as indices (output)
|
||||
// palette: pointer to color palette (output)
|
||||
// pPalette256: new color palette with 256 colors max.
|
||||
// depthMax: maximum depth of decision tree for colors quantization (sets number of colors)
|
||||
// dithering: 0 (no dithering), 1: Floyd-Steinberg dithering, else: Sierra dithering
|
||||
static
|
||||
uint32_t rgb_to_index(const uint8_t* pImageDataRGB, uint32_t numPixel, uint32_t width, cgif_chan_fmt fmtChan, uint8_t* pImageData, uint8_t* pPalette256, uint8_t depthMax, uint8_t dithering, int* pHasAlpha, uint8_t* pBef, cgif_chan_fmt befFmtChan) {
|
||||
uint32_t i,j,h;
|
||||
uint32_t cntCollision; // count the number of collisions
|
||||
uint32_t cnt = 0; // count the number of colors
|
||||
uint32_t tableSize = 262147; // initial size of the hash table
|
||||
uint32_t tableSizeNew; // size of hash-table when increasing its size
|
||||
tableSize = getNextPrimePower2(tableSize); // increase table size to next prime number
|
||||
int hasAlpha = 0;
|
||||
uint8_t transIndex;
|
||||
uint32_t* colIdx = malloc(sizeof(uint32_t)*tableSize); // index of the color
|
||||
uint32_t* frequ = malloc(sizeof(uint32_t)*tableSize); // frequency of colors (histogram)
|
||||
uint8_t* hashTable = malloc(3 * tableSize); // stores RGB vales at position determined by hash
|
||||
uint8_t* indexUsed = malloc(tableSize); // which part of the hash table was already used
|
||||
uint8_t* hashTable_new; // for new hash table
|
||||
uint8_t* indexUsed_new; // for new hash table
|
||||
uint32_t* frequ_new; // for new hash table
|
||||
uint8_t* pPalette = malloc(3 * tableSize); // color palette with all the colors (before color quantization)
|
||||
const uint8_t sizePixel = fmtChan; // number of bytes for one pixel (e.g. 3 for RGB, 4 for RGBa)
|
||||
memset(pPalette, 0, 3 * tableSize); // unused part of color table is uninitialized otheriwse
|
||||
memset(indexUsed, 0, tableSize); // initially no entry in hash-table is used
|
||||
|
||||
for(i = 0; i < numPixel; ++i) {
|
||||
h = col_hash(&pImageDataRGB[sizePixel * i], hashTable, indexUsed, tableSize, fmtChan, &cntCollision);
|
||||
if(h == -1) { // -1 means that the user has set a user-defined transparency (alpha channel)
|
||||
hasAlpha = 1;
|
||||
continue; // do not take this pixel into account (e.g. alpha channel present)
|
||||
}
|
||||
if(indexUsed[h] == 0) {
|
||||
memcpy(&hashTable[3 * h], &pImageDataRGB[sizePixel * i], 3); // add new color to hash table
|
||||
memcpy(&pPalette[3 * cnt], &pImageDataRGB[sizePixel * i], 3); // add new color to palette
|
||||
indexUsed[h] = 1; // mark hash table entry as used
|
||||
colIdx[h] = cnt; // assign index to color
|
||||
frequ[h] = 1; // the new color occured 1x (1st time)
|
||||
++cnt; // one new color added
|
||||
} else { // if color exists already
|
||||
frequ[h] += 1; // increment color histrogram
|
||||
}
|
||||
// resize the hash table (if more than half-full)
|
||||
if((cnt > (tableSize >> 1) || cntCollision > MAX_COLLISIONS) && tableSize < MAX_TABLE_SIZE) {
|
||||
tableSizeNew = getNextPrimePower2(tableSize); // increase table size to the next prime number above the next power of two
|
||||
pPalette = realloc(pPalette, 3 * tableSizeNew);
|
||||
colIdx = realloc(colIdx, sizeof(uint32_t) * tableSizeNew);
|
||||
hashTable_new = malloc(3 * tableSizeNew);
|
||||
indexUsed_new = malloc(tableSizeNew);
|
||||
frequ_new = malloc(sizeof(uint32_t) * tableSizeNew);
|
||||
memset(indexUsed_new, 0, tableSizeNew);
|
||||
cnt = 0;
|
||||
for(j = 0; j < tableSize; ++j) { // TBD: wouldn't it be easier to loop over pPalette and also leave pPalette in place?, if indexUsed is also unnecessary then
|
||||
if(indexUsed[j] == 1){
|
||||
h = col_hash(&hashTable[3 * j], hashTable_new, indexUsed_new, tableSizeNew, 3, &cntCollision); // recompute hash with new table size
|
||||
memcpy(&hashTable_new[3 * h], &hashTable[3 * j], 3); // put value from old hash table to right position of the new one
|
||||
memcpy(&pPalette[3 * cnt], &hashTable[3 * j], 3);
|
||||
indexUsed_new[h] = 1;
|
||||
frequ_new[h] = frequ[j];
|
||||
colIdx[h] = cnt;
|
||||
++cnt;
|
||||
}
|
||||
}
|
||||
tableSize = tableSizeNew;
|
||||
free(hashTable); // free old hash table that is not used anymore
|
||||
free(indexUsed);
|
||||
free(frequ);
|
||||
hashTable = hashTable_new; // pass pointer to new hash table
|
||||
indexUsed = indexUsed_new;
|
||||
frequ = frequ_new;
|
||||
}
|
||||
}
|
||||
|
||||
const uint16_t colMax = (1uL << depthMax) - 1; // maximum number of colors (-1 to leave space for transparency), disadvantage (TBD): quantization for static image with 256 colors and no alpha channel unnecessary
|
||||
if(cnt > colMax) { // color-quantization is needed
|
||||
uint32_t* pFrequDense = hash_to_dense(pPalette, frequ, cnt, hashTable, indexUsed, tableSize, fmtChan);
|
||||
treeNode* root = create_decision_tree(pPalette, pFrequDense, pPalette256, cnt, colMax, depthMax); // create decision tree (dynamic, splits along rgb-dimension with highest variance)
|
||||
float* pImageDataRGBfloat = malloc(fmtChan * numPixel * sizeof(float)); // TBD fmtChan + only when hasAlpha
|
||||
for(i = 0; i < fmtChan * numPixel; ++i){
|
||||
pImageDataRGBfloat[i] = pImageDataRGB[i];
|
||||
}
|
||||
transIndex = colMax;
|
||||
get_quantized_dithered_image(pImageData, pImageDataRGBfloat, pPalette256, root, numPixel, width, dithering, transIndex, fmtChan, pBef, befFmtChan, hasAlpha); // do color quantization and dithering
|
||||
free(pImageDataRGBfloat); // RGB image is not needed anymore
|
||||
free_decision_tree(root); // tree for color quantization is not needed anymore
|
||||
free(pFrequDense);
|
||||
cnt = colMax;
|
||||
} else { // no color-quantization is needed if the number of colors is small enough
|
||||
transIndex = cnt;
|
||||
for(i = 0; i < numPixel; ++i) {
|
||||
if(!hasAlpha && pBef) {
|
||||
if(memcmp(&pImageData[sizePixel * i], &pBef[befFmtChan * i], 3) == 0) {
|
||||
pImageData[i] = transIndex;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
h = col_hash(&pImageDataRGB[sizePixel * i], hashTable, indexUsed, tableSize, fmtChan, &cntCollision);
|
||||
pImageData[i] = (h == -1) ? transIndex : colIdx[h];
|
||||
}
|
||||
memcpy(pPalette256, pPalette, 3 * 256); // keep the color palette (no quantization)
|
||||
}
|
||||
*pHasAlpha = hasAlpha;
|
||||
free(colIdx);
|
||||
free(hashTable);
|
||||
free(indexUsed);
|
||||
free(pPalette);
|
||||
free(frequ);
|
||||
return cnt; // return number of colors found
|
||||
}
|
||||
|
||||
CGIFrgb* cgif_rgb_newgif(const CGIFrgb_Config* pConfig) {
|
||||
CGIF_Config idxConfig = {0};
|
||||
CGIFrgb* pGIFrgb;
|
||||
|
||||
pGIFrgb = malloc(sizeof(CGIFrgb));
|
||||
memset(pGIFrgb, 0, sizeof(CGIFrgb));
|
||||
idxConfig.pWriteFn = pConfig->pWriteFn;
|
||||
idxConfig.pContext = pConfig->pContext;
|
||||
idxConfig.path = pConfig->path;
|
||||
idxConfig.numLoops = pConfig->numLoops;
|
||||
idxConfig.width = pConfig->width;
|
||||
idxConfig.height = pConfig->height;
|
||||
idxConfig.attrFlags = CGIF_ATTR_IS_ANIMATED | CGIF_ATTR_NO_GLOBAL_TABLE;
|
||||
pGIFrgb->pGIF = cgif_newgif(&idxConfig);
|
||||
if(pGIFrgb->pGIF == NULL) {
|
||||
free(pGIFrgb);
|
||||
return NULL;
|
||||
}
|
||||
pGIFrgb->config = *pConfig;
|
||||
return pGIFrgb;
|
||||
}
|
||||
|
||||
cgif_result cgif_rgb_addframe(CGIFrgb* pGIF, const CGIFrgb_FrameConfig* pConfig) {
|
||||
uint8_t aPalette[256 * 3];
|
||||
memset(aPalette, 0, 256 * 3); // rgb_to_index does not necessarily fill/use all palette entries (-> initialize aPalette to avoid underfined behaviour)
|
||||
CGIF_FrameConfig fConfig = {0};
|
||||
uint8_t* pNewBef;
|
||||
const uint16_t imageWidth = pGIF->config.width;
|
||||
const uint16_t imageHeight = pGIF->config.height;
|
||||
const uint32_t numPixel = MULU16(imageWidth, imageHeight);
|
||||
int hasAlpha;
|
||||
|
||||
pNewBef = malloc(pConfig->fmtChan * MULU16(imageWidth, imageHeight));
|
||||
memcpy(pNewBef, pConfig->pImageData, pConfig->fmtChan * MULU16(imageWidth, imageHeight));
|
||||
fConfig.pLocalPalette = aPalette;
|
||||
fConfig.pImageData = malloc(pGIF->config.width * (uint32_t)pGIF->config.height);
|
||||
fConfig.delay = pConfig->delay;
|
||||
fConfig.attrFlags = CGIF_FRAME_ATTR_USE_LOCAL_TABLE;
|
||||
|
||||
const int sizeLCT = rgb_to_index(pConfig->pImageData, numPixel, pGIF->config.width, pConfig->fmtChan, fConfig.pImageData, aPalette, 8, 1, &hasAlpha, pGIF->pBefImageData, pGIF->befFmtChan);
|
||||
fConfig.numLocalPaletteEntries = sizeLCT;
|
||||
if(hasAlpha) {
|
||||
fConfig.attrFlags |= CGIF_FRAME_ATTR_HAS_ALPHA;
|
||||
fConfig.transIndex = sizeLCT;
|
||||
} else {
|
||||
fConfig.attrFlags |= CGIF_FRAME_ATTR_HAS_SET_TRANS;
|
||||
fConfig.transIndex = sizeLCT;
|
||||
}
|
||||
cgif_result r = cgif_addframe(pGIF->pGIF, &fConfig);
|
||||
free(pGIF->pBefImageData);
|
||||
pGIF->pBefImageData = pNewBef;
|
||||
pGIF->befFmtChan = pConfig->fmtChan;
|
||||
|
||||
// cleanup
|
||||
free(fConfig.pImageData);
|
||||
return r;
|
||||
}
|
||||
|
||||
cgif_result cgif_rgb_close(CGIFrgb* pGIF) {
|
||||
cgif_result r = cgif_close(pGIF->pGIF);
|
||||
free(pGIF->pBefImageData);
|
||||
free(pGIF);
|
||||
return r;
|
||||
}
|
|
@ -1,10 +0,0 @@
|
|||
|
||||
INCLUDEPATH += $$PWD/giflib
|
||||
|
||||
SOURCES += $$PWD/giflib/dgif_lib.c \
|
||||
$$PWD/giflib/egif_lib.c \
|
||||
$$PWD/giflib/gif_err.c \
|
||||
$$PWD/giflib/gif_hash.c \
|
||||
$$PWD/giflib/gifalloc.c \
|
||||
$$PWD/giflib/quantize.c \
|
||||
$$PWD/giflib/openbsd-reallocarray.c
|
|
@ -15,17 +15,17 @@
|
|||
|
||||
## WingGifEditor
|
||||
|
||||
  本软件是基于 QT 编写的 GIF 编辑器,采用 C++ 进行开发,目的是让 Deepin 上具有简单易用的 GIF 编辑器。`Windows`上有一个十分强大都既是 GIF 录屏工具又是强大 GIF 编辑器`ScreenToGif`。但 Linux 上只有强大的 GIF 录屏工具,但没有简单好用满足基本编辑需求的 GIF 编辑器。于是乎,我给开发了一个简易的 GIF 编辑器,它具有基本的编辑功能,简单方便。
|
||||
  本软件是基于 QT 编写的 GIF 编辑器,采用 C++ 进行开发,目的是让 Deepin 上具有简单易用的 GIF 编辑器。`Windows`上有一个十分强大都既是 GIF 录屏工具又是强大 GIF 编辑器`ScreenToGif`。但 Linux 上只有强大的 GIF 录屏工具,但没有简单好用满足基本编辑需求带有图形界面的 GIF 编辑器。于是乎,我给开发了一个简易的 GIF 编辑器,它具有基本的编辑功能,简单方便。
|
||||
|
||||
  有关本软件提建议的时候不要天马行空,本软件的定位是提供简单易用满足基本需求的 GIF 编辑器,并不是像`ScreenToGif`,一是没有相关基础知识,自己一个人搞不动;二是没必要,我不想把这个软件搞个大而全,配合`Gimp`等优秀的图像编辑器作为辅助,来编辑 GIF 图像绰绰有余了。
|
||||
|
||||
### GIF 库
|
||||
|
||||
  本软件基于两个库修改定制而得,一个负责读:`QtGifImage`,另一个负责写:`GifEncoder`。
|
||||
  本软件基于两个库修改定制而得,一个负责读:`QtGifImage`,另一个负责写:`cgif`。
|
||||
|
||||
  `QtGifImage`是`Qt`上的`GIF`读写库,可以将`GIF`解析为`QImage`序列,基于`giflib`。这个我改动比较大,只保留了读取部分,并与我的软件做了适配(这个库写`GIF`图片的质量有点太差),仓库 [链接](https://github.com/jiangcaiyang/QtGifImage) 。
|
||||
|
||||
  `GifEncoder`是一个能够提供高质量编码的`GIF`编码库,使用神经网络算法。为了实现基本的压缩,我进行略微的修改定制得到,仓库 [链接](https://github.com/xiaozhuai/GifEncoder) 。
|
||||
  `cgif`一个快速轻量级的GIF编码器,可以创建 GIF 动画和图像,仓库 [链接](https://github.com/dloebl/cgif) 。
|
||||
|
||||
### 编译安装
|
||||
|
||||
|
|
|
@ -35,7 +35,20 @@ SOURCES += \
|
|||
UndoCommand/replaceframecommand.cpp \
|
||||
UndoCommand/cropimagecommand.cpp \
|
||||
Dialog/waitingdialog.cpp \
|
||||
settingdialog.cpp \
|
||||
GifImage/decoder/gifdecoder.cpp \
|
||||
GifImage/decoder/giflib/dgif_lib.c \
|
||||
GifImage/decoder/giflib/egif_lib.c \
|
||||
GifImage/decoder/giflib/gif_err.c \
|
||||
GifImage/decoder/giflib/gif_font.c \
|
||||
GifImage/decoder/giflib/gif_hash.c \
|
||||
GifImage/decoder/giflib/gifalloc.c \
|
||||
GifImage/decoder/giflib/openbsd-reallocarray.c \
|
||||
GifImage/decoder/giflib/quantize.c \
|
||||
GifImage/encoder/src/cgif.c \
|
||||
GifImage/encoder/src/cgif_raw.c \
|
||||
GifImage/encoder/gifencoder.cpp \
|
||||
GifImage/encoder/src/cgif_rgb.c \
|
||||
Dialog/settingdialog.cpp \
|
||||
Class/settings.cpp
|
||||
|
||||
RESOURCES += resources.qrc
|
||||
|
@ -68,6 +81,14 @@ HEADERS += Dialog/mainwindow.h \
|
|||
UndoCommand/replaceframecommand.h \
|
||||
UndoCommand/cropimagecommand.h \
|
||||
Dialog/waitingdialog.h \
|
||||
GifImage/decoder/giflib/gif_hash.h \
|
||||
GifImage/decoder/giflib/gif_lib.h \
|
||||
GifImage/decoder/giflib/gif_lib_private.h \
|
||||
GifImage/decoder/gifdecoder.h \
|
||||
GifImage/encoder/inc/cgif.h \
|
||||
GifImage/encoder/inc/cgif_raw.h \
|
||||
GifImage/encoder/gifencoder.h \
|
||||
Dialog/settingdialog.h \
|
||||
Class/settings.h
|
||||
|
||||
DISTFILES +=
|
||||
|
@ -75,4 +96,5 @@ DISTFILES +=
|
|||
TRANSLATIONS += \
|
||||
$$PWD/lang/zh.ts
|
||||
|
||||
include($$PWD/GifImage/WingGif.pri)
|
||||
INCLUDEPATH += $$PWD/GifImage/decoder/giflib
|
||||
INCLUDEPATH += $$PWD/GifImage/encoder/inc
|
||||
|
|
|
@ -1,16 +1,16 @@
|
|||
## WingGifEditor
|
||||
|
||||
  本软件是基于 QT 编写的 GIF 编辑器,采用 C++ 进行开发,目的是让 Deepin 上具有简单易用的 GIF 编辑器。`Windows`上有一个十分强大都既是 GIF 录屏工具又是强大 GIF 编辑器`ScreenToGif`。但 Linux 上只有强大的 GIF 录屏工具,但没有简单好用满足基本编辑需求的 GIF 编辑器。于是乎,我给开发了一个简易的 GIF 编辑器,它具有基本的编辑功能,简单方便。
|
||||
  本软件是基于 QT 编写的 GIF 编辑器,采用 C++ 进行开发,目的是让 Deepin 上具有简单易用的 GIF 编辑器。`Windows`上有一个十分强大都既是 GIF 录屏工具又是强大 GIF 编辑器`ScreenToGif`。但 Linux 上只有强大的 GIF 录屏工具,但没有简单好用满足基本编辑需求带有图形界面的 GIF 编辑器。于是乎,我给开发了一个简易的 GIF 编辑器,它具有基本的编辑功能,简单方便。
|
||||
|
||||
  有关本软件提建议的时候不要天马行空,本软件的定位是提供简单易用满足基本需求的 GIF 编辑器,并不是像`ScreenToGif`,一是没有相关基础知识,自己一个人搞不动;二是没必要,我不想把这个软件搞个大而全,配合`Gimp`等优秀的图像编辑器作为辅助,来编辑 GIF 图像绰绰有余了。
|
||||
|
||||
### GIF 库
|
||||
|
||||
  本软件基于两个库修改定制而得,一个负责读:`QtGifImage`,另一个负责写:`GifEncoder`。
|
||||
  本软件基于两个库修改定制而得,一个负责读:`QtGifImage`,另一个负责写:`cgif`。
|
||||
|
||||
  `QtGifImage`是`Qt`上的`GIF`读写库,可以将`GIF`解析为`QImage`序列,基于`giflib`。这个我改动比较大,只保留了读取部分,并与我的软件做了适配(这个库写`GIF`图片的质量有点太差),仓库 [链接](https://github.com/jiangcaiyang/QtGifImage) 。
|
||||
|
||||
  `GifEncoder`是一个能够提供高质量编码的`GIF`编码库,使用神经网络算法。为了实现基本的压缩,我进行略微的修改定制得到,仓库 [链接](https://github.com/xiaozhuai/GifEncoder) 。
|
||||
  `cgif`一个快速轻量级的GIF编码器,可以创建 GIF 动画和图像,仓库 [链接](https://github.com/dloebl/cgif) 。
|
||||
|
||||
### 编译安装
|
||||
|
||||
|
|
BIN
lang/zh.qm
BIN
lang/zh.qm
Binary file not shown.
486
lang/zh.ts
486
lang/zh.ts
|
@ -81,612 +81,602 @@
|
|||
<context>
|
||||
<name>GifDecoder</name>
|
||||
<message>
|
||||
<location filename="../GifImage/decoder/gifdecoder.cpp" line="160"/>
|
||||
<location filename="../GifImage/decoder/gifdecoder.cpp" line="190"/>
|
||||
<location filename="../GifImage/decoder/gifdecoder.cpp" line="166"/>
|
||||
<location filename="../GifImage/decoder/gifdecoder.cpp" line="196"/>
|
||||
<source>Failed to open given file</source>
|
||||
<translation>打开所给文件失败!</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../GifImage/decoder/gifdecoder.cpp" line="163"/>
|
||||
<location filename="../GifImage/decoder/gifdecoder.cpp" line="169"/>
|
||||
<source>Failed to write to given file</source>
|
||||
<translation>无法写入文件!</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../GifImage/decoder/gifdecoder.cpp" line="166"/>
|
||||
<location filename="../GifImage/decoder/gifdecoder.cpp" line="172"/>
|
||||
<source>Screen descriptor has already been set</source>
|
||||
<translation>屏幕描述符已被设置!</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../GifImage/decoder/gifdecoder.cpp" line="169"/>
|
||||
<location filename="../GifImage/decoder/gifdecoder.cpp" line="175"/>
|
||||
<source>Image descriptor is still active</source>
|
||||
<translation>图像描述符仍处于活跃状态!</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../GifImage/decoder/gifdecoder.cpp" line="172"/>
|
||||
<location filename="../GifImage/decoder/gifdecoder.cpp" line="205"/>
|
||||
<location filename="../GifImage/decoder/gifdecoder.cpp" line="178"/>
|
||||
<location filename="../GifImage/decoder/gifdecoder.cpp" line="211"/>
|
||||
<source>Neither global nor local color map</source>
|
||||
<translation>没有颜色表!</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../GifImage/decoder/gifdecoder.cpp" line="175"/>
|
||||
<location filename="../GifImage/decoder/gifdecoder.cpp" line="211"/>
|
||||
<location filename="../GifImage/decoder/gifdecoder.cpp" line="181"/>
|
||||
<location filename="../GifImage/decoder/gifdecoder.cpp" line="217"/>
|
||||
<source>Number of pixels bigger than width * height</source>
|
||||
<translation>像素数超过宽度和高度积</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../GifImage/decoder/gifdecoder.cpp" line="178"/>
|
||||
<location filename="../GifImage/decoder/gifdecoder.cpp" line="214"/>
|
||||
<location filename="../GifImage/decoder/gifdecoder.cpp" line="184"/>
|
||||
<location filename="../GifImage/decoder/gifdecoder.cpp" line="220"/>
|
||||
<source>Failed to allocate required memory</source>
|
||||
<translation>申请所需内存失败!</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../GifImage/decoder/gifdecoder.cpp" line="181"/>
|
||||
<location filename="../GifImage/decoder/gifdecoder.cpp" line="187"/>
|
||||
<source>Write failed (disk full?)</source>
|
||||
<translation>写入失败,可能由于磁盘已满!</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../GifImage/decoder/gifdecoder.cpp" line="184"/>
|
||||
<location filename="../GifImage/decoder/gifdecoder.cpp" line="217"/>
|
||||
<location filename="../GifImage/decoder/gifdecoder.cpp" line="190"/>
|
||||
<location filename="../GifImage/decoder/gifdecoder.cpp" line="223"/>
|
||||
<source>Failed to close given file</source>
|
||||
<translation>关闭文件失败!</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../GifImage/decoder/gifdecoder.cpp" line="187"/>
|
||||
<location filename="../GifImage/decoder/gifdecoder.cpp" line="193"/>
|
||||
<source>Given file was not opened for write</source>
|
||||
<translation>文件不可写!</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../GifImage/decoder/gifdecoder.cpp" line="193"/>
|
||||
<location filename="../GifImage/decoder/gifdecoder.cpp" line="199"/>
|
||||
<source>Failed to read from given file</source>
|
||||
<translation>从文件读取数据失败!</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../GifImage/decoder/gifdecoder.cpp" line="196"/>
|
||||
<location filename="../GifImage/decoder/gifdecoder.cpp" line="202"/>
|
||||
<source>Data is not in GIF format</source>
|
||||
<translation>数据不是 GIF 格式!</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../GifImage/decoder/gifdecoder.cpp" line="199"/>
|
||||
<location filename="../GifImage/decoder/gifdecoder.cpp" line="205"/>
|
||||
<source>No screen descriptor detected</source>
|
||||
<translation>没有检测到屏幕描述符!</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../GifImage/decoder/gifdecoder.cpp" line="202"/>
|
||||
<location filename="../GifImage/decoder/gifdecoder.cpp" line="208"/>
|
||||
<source>No Image Descriptor detected</source>
|
||||
<translation>没有检测到图像描述符!</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../GifImage/decoder/gifdecoder.cpp" line="208"/>
|
||||
<location filename="../GifImage/decoder/gifdecoder.cpp" line="214"/>
|
||||
<source>Wrong record type detected</source>
|
||||
<translation>检测到错误的记录类型!</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../GifImage/decoder/gifdecoder.cpp" line="220"/>
|
||||
<location filename="../GifImage/decoder/gifdecoder.cpp" line="226"/>
|
||||
<source>Given file was not opened for read</source>
|
||||
<translation>所给文件无法打开读取数据!</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../GifImage/decoder/gifdecoder.cpp" line="223"/>
|
||||
<location filename="../GifImage/decoder/gifdecoder.cpp" line="229"/>
|
||||
<source>Image is defective, decoding aborted</source>
|
||||
<translation>图像有缺陷,解码中止!</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../GifImage/decoder/gifdecoder.cpp" line="226"/>
|
||||
<location filename="../GifImage/decoder/gifdecoder.cpp" line="232"/>
|
||||
<source>Image EOF detected before image complete</source>
|
||||
<translation>在图像完成之前检测到图像结尾!</translation>
|
||||
</message>
|
||||
</context>
|
||||
<context>
|
||||
<name>GifImage</name>
|
||||
<message>
|
||||
<source>Unknown Error!</source>
|
||||
<translation type="vanished">未知错误!</translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>UnSolvedBug</source>
|
||||
<translation type="vanished">恭喜你触发了 Bug</translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>GIFUnSolveBugInfo</source>
|
||||
<translation type="vanished">我不清楚怎么解决,这个是调用 deconstructImages 函数失败导致的结果。这个函数可以大大降低 GIF 图片的大小,但我不知道为什么,保存使用深度录屏 GIF 会抛出异常。该程序将会执行缓解措施,但保存的文件大小巨大。</translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Error</source>
|
||||
<translation type="vanished">出错了</translation>
|
||||
</message>
|
||||
</context>
|
||||
<context>
|
||||
<name>MainWindow</name>
|
||||
<message>
|
||||
<location filename="../Dialog/mainwindow.cpp" line="52"/>
|
||||
<location filename="../Dialog/mainwindow.cpp" line="82"/>
|
||||
<location filename="../Dialog/mainwindow.cpp" line="53"/>
|
||||
<location filename="../Dialog/mainwindow.cpp" line="83"/>
|
||||
<source>WingGifEditor</source>
|
||||
<translation>羽云 GIF 编辑器</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../Dialog/mainwindow.cpp" line="147"/>
|
||||
<location filename="../Dialog/mainwindow.cpp" line="148"/>
|
||||
<source>File</source>
|
||||
<translation>文件</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../Dialog/mainwindow.cpp" line="151"/>
|
||||
<location filename="../Dialog/mainwindow.cpp" line="152"/>
|
||||
<source>New</source>
|
||||
<translation>新建</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../Dialog/mainwindow.cpp" line="152"/>
|
||||
<location filename="../Dialog/mainwindow.cpp" line="345"/>
|
||||
<location filename="../Dialog/mainwindow.cpp" line="153"/>
|
||||
<location filename="../Dialog/mainwindow.cpp" line="346"/>
|
||||
<source>NewFromPics</source>
|
||||
<translation>从图片序列</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../Dialog/mainwindow.cpp" line="153"/>
|
||||
<location filename="../Dialog/mainwindow.cpp" line="346"/>
|
||||
<location filename="../Dialog/mainwindow.cpp" line="154"/>
|
||||
<location filename="../Dialog/mainwindow.cpp" line="347"/>
|
||||
<source>NewFromGifs</source>
|
||||
<translation>从 GIF 序列</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../Dialog/mainwindow.cpp" line="156"/>
|
||||
<location filename="../Dialog/mainwindow.cpp" line="354"/>
|
||||
<location filename="../Dialog/mainwindow.cpp" line="157"/>
|
||||
<location filename="../Dialog/mainwindow.cpp" line="355"/>
|
||||
<source>Open</source>
|
||||
<translation>打开</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../Dialog/mainwindow.cpp" line="158"/>
|
||||
<location filename="../Dialog/mainwindow.cpp" line="355"/>
|
||||
<location filename="../Dialog/mainwindow.cpp" line="159"/>
|
||||
<location filename="../Dialog/mainwindow.cpp" line="356"/>
|
||||
<source>Save</source>
|
||||
<translation>保存</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../Dialog/mainwindow.cpp" line="161"/>
|
||||
<location filename="../Dialog/mainwindow.cpp" line="357"/>
|
||||
<location filename="../Dialog/mainwindow.cpp" line="162"/>
|
||||
<location filename="../Dialog/mainwindow.cpp" line="358"/>
|
||||
<source>SaveAs</source>
|
||||
<translation>另存为</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../Dialog/mainwindow.cpp" line="164"/>
|
||||
<location filename="../Dialog/mainwindow.cpp" line="359"/>
|
||||
<location filename="../Dialog/mainwindow.cpp" line="165"/>
|
||||
<location filename="../Dialog/mainwindow.cpp" line="360"/>
|
||||
<source>Export</source>
|
||||
<translation>导出</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../Dialog/mainwindow.cpp" line="167"/>
|
||||
<location filename="../Dialog/mainwindow.cpp" line="168"/>
|
||||
<source>Close</source>
|
||||
<translation>关闭</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../Dialog/mainwindow.cpp" line="173"/>
|
||||
<location filename="../Dialog/mainwindow.cpp" line="174"/>
|
||||
<source>Edit</source>
|
||||
<translation>编辑</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../Dialog/mainwindow.cpp" line="175"/>
|
||||
<location filename="../Dialog/mainwindow.cpp" line="362"/>
|
||||
<location filename="../Dialog/mainwindow.cpp" line="176"/>
|
||||
<location filename="../Dialog/mainwindow.cpp" line="363"/>
|
||||
<source>Undo</source>
|
||||
<translation>撤销</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../Dialog/mainwindow.cpp" line="179"/>
|
||||
<location filename="../Dialog/mainwindow.cpp" line="365"/>
|
||||
<location filename="../Dialog/mainwindow.cpp" line="180"/>
|
||||
<location filename="../Dialog/mainwindow.cpp" line="366"/>
|
||||
<source>Redo</source>
|
||||
<translation>恢复</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../Dialog/mainwindow.cpp" line="184"/>
|
||||
<location filename="../Dialog/mainwindow.cpp" line="368"/>
|
||||
<location filename="../Dialog/mainwindow.cpp" line="185"/>
|
||||
<location filename="../Dialog/mainwindow.cpp" line="369"/>
|
||||
<source>Cut</source>
|
||||
<translation>剪切</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../Dialog/mainwindow.cpp" line="187"/>
|
||||
<location filename="../Dialog/mainwindow.cpp" line="370"/>
|
||||
<location filename="../Dialog/mainwindow.cpp" line="188"/>
|
||||
<location filename="../Dialog/mainwindow.cpp" line="371"/>
|
||||
<source>Copy</source>
|
||||
<translation>复制</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../Dialog/mainwindow.cpp" line="190"/>
|
||||
<location filename="../Dialog/mainwindow.cpp" line="372"/>
|
||||
<location filename="../Dialog/mainwindow.cpp" line="191"/>
|
||||
<location filename="../Dialog/mainwindow.cpp" line="373"/>
|
||||
<source>Paste</source>
|
||||
<translation>粘贴</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../Dialog/mainwindow.cpp" line="193"/>
|
||||
<location filename="../Dialog/mainwindow.cpp" line="374"/>
|
||||
<location filename="../Dialog/mainwindow.cpp" line="194"/>
|
||||
<location filename="../Dialog/mainwindow.cpp" line="375"/>
|
||||
<source>Delete</source>
|
||||
<translation>删除</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../Dialog/mainwindow.cpp" line="197"/>
|
||||
<location filename="../Dialog/mainwindow.cpp" line="376"/>
|
||||
<location filename="../Dialog/mainwindow.cpp" line="198"/>
|
||||
<location filename="../Dialog/mainwindow.cpp" line="377"/>
|
||||
<source>SelectAll</source>
|
||||
<translation>全选</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../Dialog/mainwindow.cpp" line="200"/>
|
||||
<location filename="../Dialog/mainwindow.cpp" line="378"/>
|
||||
<location filename="../Dialog/mainwindow.cpp" line="201"/>
|
||||
<location filename="../Dialog/mainwindow.cpp" line="379"/>
|
||||
<source>Deselect</source>
|
||||
<translation>取消选择</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../Dialog/mainwindow.cpp" line="203"/>
|
||||
<location filename="../Dialog/mainwindow.cpp" line="380"/>
|
||||
<location filename="../Dialog/mainwindow.cpp" line="204"/>
|
||||
<location filename="../Dialog/mainwindow.cpp" line="381"/>
|
||||
<source>ReverseSelection</source>
|
||||
<translation>反选</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../Dialog/mainwindow.cpp" line="207"/>
|
||||
<location filename="../Dialog/mainwindow.cpp" line="382"/>
|
||||
<location filename="../Dialog/mainwindow.cpp" line="941"/>
|
||||
<location filename="../Dialog/mainwindow.cpp" line="208"/>
|
||||
<location filename="../Dialog/mainwindow.cpp" line="383"/>
|
||||
<location filename="../Dialog/mainwindow.cpp" line="860"/>
|
||||
<source>Goto</source>
|
||||
<translation>跳转</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../Dialog/mainwindow.cpp" line="213"/>
|
||||
<location filename="../Dialog/mainwindow.cpp" line="220"/>
|
||||
<location filename="../Dialog/mainwindow.cpp" line="436"/>
|
||||
<location filename="../Dialog/mainwindow.cpp" line="214"/>
|
||||
<location filename="../Dialog/mainwindow.cpp" line="221"/>
|
||||
<location filename="../Dialog/mainwindow.cpp" line="437"/>
|
||||
<source>Play</source>
|
||||
<translation>播放</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../Dialog/mainwindow.cpp" line="214"/>
|
||||
<location filename="../Dialog/mainwindow.cpp" line="432"/>
|
||||
<location filename="../Dialog/mainwindow.cpp" line="215"/>
|
||||
<location filename="../Dialog/mainwindow.cpp" line="433"/>
|
||||
<source>FirstFrame</source>
|
||||
<translation>起始</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../Dialog/mainwindow.cpp" line="217"/>
|
||||
<location filename="../Dialog/mainwindow.cpp" line="434"/>
|
||||
<location filename="../Dialog/mainwindow.cpp" line="218"/>
|
||||
<location filename="../Dialog/mainwindow.cpp" line="435"/>
|
||||
<source>LastFrame</source>
|
||||
<translation>上一个</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../Dialog/mainwindow.cpp" line="223"/>
|
||||
<location filename="../Dialog/mainwindow.cpp" line="438"/>
|
||||
<location filename="../Dialog/mainwindow.cpp" line="224"/>
|
||||
<location filename="../Dialog/mainwindow.cpp" line="439"/>
|
||||
<source>Stop</source>
|
||||
<translation>暂停</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../Dialog/mainwindow.cpp" line="225"/>
|
||||
<location filename="../Dialog/mainwindow.cpp" line="440"/>
|
||||
<location filename="../Dialog/mainwindow.cpp" line="226"/>
|
||||
<location filename="../Dialog/mainwindow.cpp" line="441"/>
|
||||
<source>NextFrame</source>
|
||||
<translation>下一个</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../Dialog/mainwindow.cpp" line="228"/>
|
||||
<location filename="../Dialog/mainwindow.cpp" line="442"/>
|
||||
<location filename="../Dialog/mainwindow.cpp" line="229"/>
|
||||
<location filename="../Dialog/mainwindow.cpp" line="443"/>
|
||||
<source>EndFrame</source>
|
||||
<translation>结束</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../Dialog/mainwindow.cpp" line="235"/>
|
||||
<location filename="../Dialog/mainwindow.cpp" line="236"/>
|
||||
<source>Picture</source>
|
||||
<translation>图像</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../Dialog/mainwindow.cpp" line="236"/>
|
||||
<location filename="../Dialog/mainwindow.cpp" line="395"/>
|
||||
<location filename="../Dialog/mainwindow.cpp" line="237"/>
|
||||
<location filename="../Dialog/mainwindow.cpp" line="396"/>
|
||||
<source>ReduceFrame</source>
|
||||
<translation>删除帧</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../Dialog/mainwindow.cpp" line="239"/>
|
||||
<location filename="../Dialog/mainwindow.cpp" line="397"/>
|
||||
<location filename="../Dialog/mainwindow.cpp" line="240"/>
|
||||
<location filename="../Dialog/mainwindow.cpp" line="398"/>
|
||||
<source>DeleteBefore</source>
|
||||
<translation>删除选中帧之前所有</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../Dialog/mainwindow.cpp" line="242"/>
|
||||
<location filename="../Dialog/mainwindow.cpp" line="399"/>
|
||||
<location filename="../Dialog/mainwindow.cpp" line="243"/>
|
||||
<location filename="../Dialog/mainwindow.cpp" line="400"/>
|
||||
<source>DeleteAfter</source>
|
||||
<translation>删除选中帧之后所有</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../Dialog/mainwindow.cpp" line="246"/>
|
||||
<location filename="../Dialog/mainwindow.cpp" line="401"/>
|
||||
<location filename="../Dialog/mainwindow.cpp" line="247"/>
|
||||
<location filename="../Dialog/mainwindow.cpp" line="402"/>
|
||||
<source>MoveLeft</source>
|
||||
<translation>左移选中帧</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../Dialog/mainwindow.cpp" line="249"/>
|
||||
<location filename="../Dialog/mainwindow.cpp" line="403"/>
|
||||
<location filename="../Dialog/mainwindow.cpp" line="250"/>
|
||||
<location filename="../Dialog/mainwindow.cpp" line="404"/>
|
||||
<source>MoveRight</source>
|
||||
<translation>右移选中帧</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../Dialog/mainwindow.cpp" line="252"/>
|
||||
<location filename="../Dialog/mainwindow.cpp" line="405"/>
|
||||
<location filename="../Dialog/mainwindow.cpp" line="253"/>
|
||||
<location filename="../Dialog/mainwindow.cpp" line="406"/>
|
||||
<source>Reverse</source>
|
||||
<translation>反转帧</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../Dialog/mainwindow.cpp" line="256"/>
|
||||
<location filename="../Dialog/mainwindow.cpp" line="408"/>
|
||||
<location filename="../Dialog/mainwindow.cpp" line="257"/>
|
||||
<location filename="../Dialog/mainwindow.cpp" line="409"/>
|
||||
<source>SetDelay</source>
|
||||
<translation>设置延时</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../Dialog/mainwindow.cpp" line="259"/>
|
||||
<location filename="../Dialog/mainwindow.cpp" line="410"/>
|
||||
<location filename="../Dialog/mainwindow.cpp" line="260"/>
|
||||
<location filename="../Dialog/mainwindow.cpp" line="411"/>
|
||||
<source>InsertPics</source>
|
||||
<translation>插入图片序列</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../Dialog/mainwindow.cpp" line="261"/>
|
||||
<location filename="../Dialog/mainwindow.cpp" line="412"/>
|
||||
<location filename="../Dialog/mainwindow.cpp" line="262"/>
|
||||
<location filename="../Dialog/mainwindow.cpp" line="413"/>
|
||||
<source>MergeGIfs</source>
|
||||
<translation>合并 GIF 序列</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../Dialog/mainwindow.cpp" line="263"/>
|
||||
<location filename="../Dialog/mainwindow.cpp" line="414"/>
|
||||
<location filename="../Dialog/mainwindow.cpp" line="264"/>
|
||||
<location filename="../Dialog/mainwindow.cpp" line="415"/>
|
||||
<source>ScaleGif</source>
|
||||
<translation>缩放</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../Dialog/mainwindow.cpp" line="266"/>
|
||||
<location filename="../Dialog/mainwindow.cpp" line="416"/>
|
||||
<location filename="../Dialog/mainwindow.cpp" line="267"/>
|
||||
<location filename="../Dialog/mainwindow.cpp" line="417"/>
|
||||
<source>CutGif</source>
|
||||
<translation>裁切</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../Dialog/mainwindow.cpp" line="270"/>
|
||||
<location filename="../Dialog/mainwindow.cpp" line="419"/>
|
||||
<location filename="../Dialog/mainwindow.cpp" line="271"/>
|
||||
<location filename="../Dialog/mainwindow.cpp" line="420"/>
|
||||
<source>FilpH</source>
|
||||
<translation>水平反转</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../Dialog/mainwindow.cpp" line="272"/>
|
||||
<location filename="../Dialog/mainwindow.cpp" line="421"/>
|
||||
<location filename="../Dialog/mainwindow.cpp" line="273"/>
|
||||
<location filename="../Dialog/mainwindow.cpp" line="422"/>
|
||||
<source>FlipV</source>
|
||||
<translation>垂直反转</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../Dialog/mainwindow.cpp" line="274"/>
|
||||
<location filename="../Dialog/mainwindow.cpp" line="423"/>
|
||||
<location filename="../Dialog/mainwindow.cpp" line="275"/>
|
||||
<location filename="../Dialog/mainwindow.cpp" line="424"/>
|
||||
<source>RotateLeft</source>
|
||||
<translation>逆时针旋转</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../Dialog/mainwindow.cpp" line="276"/>
|
||||
<location filename="../Dialog/mainwindow.cpp" line="425"/>
|
||||
<location filename="../Dialog/mainwindow.cpp" line="277"/>
|
||||
<location filename="../Dialog/mainwindow.cpp" line="426"/>
|
||||
<source>RotateR</source>
|
||||
<translation>顺时针旋转</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../Dialog/mainwindow.cpp" line="281"/>
|
||||
<location filename="../Dialog/mainwindow.cpp" line="282"/>
|
||||
<source>Effect</source>
|
||||
<translation>效果</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../Dialog/mainwindow.cpp" line="283"/>
|
||||
<location filename="../Dialog/mainwindow.cpp" line="449"/>
|
||||
<location filename="../Dialog/mainwindow.cpp" line="284"/>
|
||||
<location filename="../Dialog/mainwindow.cpp" line="450"/>
|
||||
<source>ExportBlank</source>
|
||||
<translation>导出毛胚</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../Dialog/mainwindow.cpp" line="285"/>
|
||||
<location filename="../Dialog/mainwindow.cpp" line="451"/>
|
||||
<location filename="../Dialog/mainwindow.cpp" line="286"/>
|
||||
<location filename="../Dialog/mainwindow.cpp" line="452"/>
|
||||
<source>ApplyModel</source>
|
||||
<translation>应用模型</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../Dialog/mainwindow.cpp" line="288"/>
|
||||
<location filename="../Dialog/mainwindow.cpp" line="454"/>
|
||||
<location filename="../Dialog/mainwindow.cpp" line="289"/>
|
||||
<location filename="../Dialog/mainwindow.cpp" line="455"/>
|
||||
<source>CreateReverse</source>
|
||||
<translation>创建逆向动画帧</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../Dialog/mainwindow.cpp" line="291"/>
|
||||
<location filename="../Dialog/mainwindow.cpp" line="456"/>
|
||||
<location filename="../Dialog/mainwindow.cpp" line="292"/>
|
||||
<location filename="../Dialog/mainwindow.cpp" line="457"/>
|
||||
<source>ScaleDelay</source>
|
||||
<translation>缩放延时</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../Dialog/mainwindow.cpp" line="294"/>
|
||||
<location filename="../Dialog/mainwindow.cpp" line="1435"/>
|
||||
<location filename="../Dialog/mainwindow.cpp" line="295"/>
|
||||
<location filename="../Dialog/mainwindow.cpp" line="1359"/>
|
||||
<source>OnionMask</source>
|
||||
<translation>设置洋葱帧</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../Dialog/mainwindow.cpp" line="299"/>
|
||||
<location filename="../Dialog/mainwindow.cpp" line="300"/>
|
||||
<source>Author</source>
|
||||
<translation>作者</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../Dialog/mainwindow.cpp" line="301"/>
|
||||
<location filename="../Dialog/mainwindow.cpp" line="385"/>
|
||||
<location filename="../Dialog/mainwindow.cpp" line="302"/>
|
||||
<location filename="../Dialog/mainwindow.cpp" line="386"/>
|
||||
<source>Setting</source>
|
||||
<translation>设置</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../Dialog/mainwindow.cpp" line="302"/>
|
||||
<location filename="../Dialog/mainwindow.cpp" line="303"/>
|
||||
<source>FullScreen</source>
|
||||
<translation>全屏</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../Dialog/mainwindow.cpp" line="305"/>
|
||||
<location filename="../Dialog/mainwindow.cpp" line="386"/>
|
||||
<location filename="../Dialog/mainwindow.cpp" line="306"/>
|
||||
<location filename="../Dialog/mainwindow.cpp" line="387"/>
|
||||
<source>About</source>
|
||||
<translation>关于</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../Dialog/mainwindow.cpp" line="306"/>
|
||||
<location filename="../Dialog/mainwindow.cpp" line="387"/>
|
||||
<location filename="../Dialog/mainwindow.cpp" line="307"/>
|
||||
<location filename="../Dialog/mainwindow.cpp" line="388"/>
|
||||
<source>Sponsor</source>
|
||||
<translation>赞助</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../Dialog/mainwindow.cpp" line="307"/>
|
||||
<location filename="../Dialog/mainwindow.cpp" line="388"/>
|
||||
<location filename="../Dialog/mainwindow.cpp" line="308"/>
|
||||
<location filename="../Dialog/mainwindow.cpp" line="389"/>
|
||||
<source>Wiki</source>
|
||||
<translation>网页 Wiki</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../Dialog/mainwindow.cpp" line="308"/>
|
||||
<location filename="../Dialog/mainwindow.cpp" line="309"/>
|
||||
<source>AboutQT</source>
|
||||
<translation>关于 QT</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../Dialog/mainwindow.cpp" line="491"/>
|
||||
<location filename="../Dialog/mainwindow.cpp" line="492"/>
|
||||
<source>InfoSave</source>
|
||||
<translation>是否保存</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../Dialog/mainwindow.cpp" line="494"/>
|
||||
<location filename="../Dialog/mainwindow.cpp" line="495"/>
|
||||
<source>InfoReadWrite</source>
|
||||
<translation>是否可写</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../Dialog/mainwindow.cpp" line="496"/>
|
||||
<location filename="../Dialog/mainwindow.cpp" line="497"/>
|
||||
<source>FitInView</source>
|
||||
<translation>缩放以适合视图</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../Dialog/mainwindow.cpp" line="660"/>
|
||||
<location filename="../Dialog/mainwindow.cpp" line="658"/>
|
||||
<source>OpenGif</source>
|
||||
<translation>打开 GIF 图片中……</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../Dialog/mainwindow.cpp" line="665"/>
|
||||
<location filename="../Dialog/mainwindow.cpp" line="663"/>
|
||||
<source>OpenError</source>
|
||||
<translation>打开错误!</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../Dialog/mainwindow.cpp" line="724"/>
|
||||
<location filename="../Dialog/mainwindow.cpp" line="722"/>
|
||||
<source>ConfirmClose</source>
|
||||
<translation>确认关闭</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../Dialog/mainwindow.cpp" line="725"/>
|
||||
<location filename="../Dialog/mainwindow.cpp" line="723"/>
|
||||
<source>ConfirmSave</source>
|
||||
<translation>有修改未保存,您确定丢弃吗?</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../Dialog/mainwindow.cpp" line="726"/>
|
||||
<location filename="../Dialog/mainwindow.cpp" line="724"/>
|
||||
<source>Untitle</source>
|
||||
<translation>未命名.gif</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../Dialog/mainwindow.cpp" line="743"/>
|
||||
<location filename="../Dialog/mainwindow.cpp" line="741"/>
|
||||
<source>%1 frame | %2 total</source>
|
||||
<translation>第 %1 帧 | 共 %2 帧</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../Dialog/mainwindow.cpp" line="850"/>
|
||||
<location filename="../Dialog/mainwindow.cpp" line="769"/>
|
||||
<source>NewFromPicsGif</source>
|
||||
<translation>正在从图片序列新建文件中……</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../Dialog/mainwindow.cpp" line="869"/>
|
||||
<location filename="../Dialog/mainwindow.cpp" line="788"/>
|
||||
<source>NewFromGifsGif</source>
|
||||
<translation>正在从 GIF 序列新建文件中……</translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>PleaseWait!</source>
|
||||
<translation type="vanished">正在处理,请等待!</translation>
|
||||
<location filename="../Dialog/mainwindow.cpp" line="1390"/>
|
||||
<source>Error</source>
|
||||
<translation>出错了</translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>ProcessSuccess</source>
|
||||
<translation type="vanished">处理完毕!</translation>
|
||||
<location filename="../Dialog/mainwindow.cpp" line="1390"/>
|
||||
<source>ImagingError</source>
|
||||
<translation>图像后处理错误!</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../Dialog/mainwindow.cpp" line="885"/>
|
||||
<location filename="../Dialog/mainwindow.cpp" line="1267"/>
|
||||
<location filename="../Dialog/mainwindow.cpp" line="1283"/>
|
||||
<location filename="../Dialog/mainwindow.cpp" line="1365"/>
|
||||
<location filename="../Dialog/mainwindow.cpp" line="1396"/>
|
||||
<location filename="../Dialog/mainwindow.cpp" line="1395"/>
|
||||
<source>InvaildTerminal</source>
|
||||
<translation>无效终端</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../Dialog/mainwindow.cpp" line="804"/>
|
||||
<location filename="../Dialog/mainwindow.cpp" line="1191"/>
|
||||
<location filename="../Dialog/mainwindow.cpp" line="1207"/>
|
||||
<location filename="../Dialog/mainwindow.cpp" line="1289"/>
|
||||
<location filename="../Dialog/mainwindow.cpp" line="1320"/>
|
||||
<source>ChooseFile</source>
|
||||
<translation>请选择文件</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../Dialog/mainwindow.cpp" line="898"/>
|
||||
<location filename="../Dialog/mainwindow.cpp" line="817"/>
|
||||
<source>InvalidGif</source>
|
||||
<translation>无效 GIF 文件!</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../Dialog/mainwindow.cpp" line="941"/>
|
||||
<location filename="../Dialog/mainwindow.cpp" line="1435"/>
|
||||
<location filename="../Dialog/mainwindow.cpp" line="860"/>
|
||||
<location filename="../Dialog/mainwindow.cpp" line="1359"/>
|
||||
<source>PleaseInputIndex</source>
|
||||
<translation>请输入跳转帧索引</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../Dialog/mainwindow.cpp" line="1037"/>
|
||||
<location filename="../Dialog/mainwindow.cpp" line="1346"/>
|
||||
<location filename="../Dialog/mainwindow.cpp" line="956"/>
|
||||
<location filename="../Dialog/mainwindow.cpp" line="1270"/>
|
||||
<source>ChooseSaveFile</source>
|
||||
<translation>请选择保存文件路径</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../Dialog/mainwindow.cpp" line="1044"/>
|
||||
<location filename="../Dialog/mainwindow.cpp" line="963"/>
|
||||
<source>SaveAsGif</source>
|
||||
<translation>正在另存为 GIF 图像中……</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../Dialog/mainwindow.cpp" line="1047"/>
|
||||
<location filename="../Dialog/mainwindow.cpp" line="965"/>
|
||||
<location filename="../Dialog/mainwindow.cpp" line="1102"/>
|
||||
<source>Reprocessing</source>
|
||||
<translation>后处理图像中……</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../Dialog/mainwindow.cpp" line="968"/>
|
||||
<source>SaveAsSuccess</source>
|
||||
<translation>另存为成功!</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../Dialog/mainwindow.cpp" line="1083"/>
|
||||
<location filename="../Dialog/mainwindow.cpp" line="1004"/>
|
||||
<source>ExportFrames</source>
|
||||
<translation>正在导出帧为图片中……</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../Dialog/mainwindow.cpp" line="1086"/>
|
||||
<location filename="../Dialog/mainwindow.cpp" line="1355"/>
|
||||
<location filename="../Dialog/mainwindow.cpp" line="1007"/>
|
||||
<location filename="../Dialog/mainwindow.cpp" line="1279"/>
|
||||
<source>ExportSuccess</source>
|
||||
<translation>导出成功!</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../Dialog/mainwindow.cpp" line="1089"/>
|
||||
<location filename="../Dialog/mainwindow.cpp" line="1010"/>
|
||||
<source>ExportFail</source>
|
||||
<translation>导出失败!</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../Dialog/mainwindow.cpp" line="1177"/>
|
||||
<location filename="../Dialog/mainwindow.cpp" line="1100"/>
|
||||
<source>SaveGif</source>
|
||||
<translation>正在保存 GIF 图像中……</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../Dialog/mainwindow.cpp" line="1181"/>
|
||||
<location filename="../Dialog/mainwindow.cpp" line="1106"/>
|
||||
<source>SaveSuccess</source>
|
||||
<translation>保存成功</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../Dialog/mainwindow.cpp" line="1228"/>
|
||||
<location filename="../Dialog/mainwindow.cpp" line="1154"/>
|
||||
<source>DelayTime</source>
|
||||
<translation>设置延时</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../Dialog/mainwindow.cpp" line="1228"/>
|
||||
<location filename="../Dialog/mainwindow.cpp" line="1154"/>
|
||||
<source>Inputms</source>
|
||||
<translation>请输入延时(单位 ms)</translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Input10ms</source>
|
||||
<translation type="vanished">请输入延时(单位 10ms)</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../Dialog/mainwindow.cpp" line="1247"/>
|
||||
<location filename="../Dialog/mainwindow.cpp" line="1172"/>
|
||||
<source>ScaleDelayTime</source>
|
||||
<translation>缩放延时</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../Dialog/mainwindow.cpp" line="1248"/>
|
||||
<location filename="../Dialog/mainwindow.cpp" line="1173"/>
|
||||
<source>InputPercent</source>
|
||||
<translation>请输入缩放比例</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../Dialog/mainwindow.cpp" line="1267"/>
|
||||
<location filename="../Dialog/mainwindow.cpp" line="1191"/>
|
||||
<source>Images (*.jpg *.tiff *.png)</source>
|
||||
<translation>图片 (*.jpg *.tiff *.png)</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../Dialog/mainwindow.cpp" line="1392"/>
|
||||
<location filename="../Dialog/mainwindow.cpp" line="1316"/>
|
||||
<source>NoSelection</source>
|
||||
<translation>未选择任何帧!</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../Dialog/mainwindow.cpp" line="1423"/>
|
||||
<location filename="../Dialog/mainwindow.cpp" line="1347"/>
|
||||
<source>InvalidModel</source>
|
||||
<translation>无效应用模型!</translation>
|
||||
</message>
|
||||
|
@ -763,44 +753,6 @@
|
|||
<source>A tiny easy2use gif editor for Deepin</source>
|
||||
<translation>一个运行在 Deepin 上小巧易用的 GIF 图片编辑器</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../settingdialog.cpp" line="4"/>
|
||||
<source>Editor</source>
|
||||
<translation>编辑器</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../settingdialog.cpp" line="5"/>
|
||||
<source>Appearance</source>
|
||||
<translation>外观</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../settingdialog.cpp" line="7"/>
|
||||
<source>Image</source>
|
||||
<translation>图像</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../settingdialog.cpp" line="8"/>
|
||||
<source>Quality</source>
|
||||
<translation>图片质量( 1 - 30 )</translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Quantizer</source>
|
||||
<translation type="vanished">编码器</translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Ditherer</source>
|
||||
<translation type="vanished">抖动器</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../settingdialog.cpp" line="10"/>
|
||||
<source>Window</source>
|
||||
<translation>窗口</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../settingdialog.cpp" line="11"/>
|
||||
<source>Window size</source>
|
||||
<translation>窗口大小状态</translation>
|
||||
</message>
|
||||
</context>
|
||||
<context>
|
||||
<name>ReduceFrameDialog</name>
|
||||
|
@ -854,81 +806,41 @@
|
|||
</message>
|
||||
</context>
|
||||
<context>
|
||||
<name>Settings</name>
|
||||
<name>SettingDialog</name>
|
||||
<message>
|
||||
<source>No</source>
|
||||
<translation type="vanished">无</translation>
|
||||
<location filename="../Dialog/settingdialog.cpp" line="9"/>
|
||||
<source>SettingDialog</source>
|
||||
<translation>设置窗口</translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>M2</source>
|
||||
<translation type="vanished">M2 抖动</translation>
|
||||
<location filename="../Dialog/settingdialog.cpp" line="10"/>
|
||||
<source>WinState</source>
|
||||
<translation>窗口状态</translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Bayer</source>
|
||||
<translation type="vanished">Bayer 抖动</translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>FloydSteinberg</source>
|
||||
<translation type="vanished">FloydSteinberg 抖动</translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Uniform</source>
|
||||
<translation type="vanished">均衡法</translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>MedianCut</source>
|
||||
<translation type="vanished">中值切割法</translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>KMeans</source>
|
||||
<translation type="vanished">K均值</translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Random</source>
|
||||
<translation type="vanished">随机法</translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Octree</source>
|
||||
<translation type="vanished">八叉树法</translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>NeuQuant</source>
|
||||
<translation type="vanished">神经量子法</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../Class/settings.cpp" line="37"/>
|
||||
<location filename="../Dialog/settingdialog.cpp" line="13"/>
|
||||
<source>Normal</source>
|
||||
<translation>正常</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../Class/settings.cpp" line="37"/>
|
||||
<location filename="../Dialog/settingdialog.cpp" line="13"/>
|
||||
<source>Maximum</source>
|
||||
<translation>最大化</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../Class/settings.cpp" line="38"/>
|
||||
<location filename="../Dialog/settingdialog.cpp" line="13"/>
|
||||
<source>Minimum</source>
|
||||
<translation>最小化</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../Class/settings.cpp" line="38"/>
|
||||
<source>Fullscreen</source>
|
||||
<translation>全屏</translation>
|
||||
<location filename="../Dialog/settingdialog.cpp" line="17"/>
|
||||
<source>Terminal</source>
|
||||
<translation>终端</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../Class/settings.cpp" line="90"/>
|
||||
<source>Cancel</source>
|
||||
<translation>取消取消</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../Class/settings.cpp" line="91"/>
|
||||
<source>Replace</source>
|
||||
<translation>替换</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../Class/settings.cpp" line="93"/>
|
||||
<source>OK</source>
|
||||
<translation>确认</translation>
|
||||
<location filename="../Dialog/settingdialog.cpp" line="23"/>
|
||||
<source>Reprocessing</source>
|
||||
<translation>后处理( %1 代表输出文件路径)</translation>
|
||||
</message>
|
||||
</context>
|
||||
<context>
|
||||
|
@ -947,12 +859,12 @@
|
|||
<context>
|
||||
<name>WaitingDialog</name>
|
||||
<message>
|
||||
<location filename="../Dialog/waitingdialog.cpp" line="10"/>
|
||||
<location filename="../Dialog/waitingdialog.cpp" line="9"/>
|
||||
<source>PleaseWait</source>
|
||||
<translation>请稍后</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../Dialog/waitingdialog.cpp" line="26"/>
|
||||
<location filename="../Dialog/waitingdialog.cpp" line="25"/>
|
||||
<source>Please Waiting...</source>
|
||||
<translation>请稍后……</translation>
|
||||
</message>
|
||||
|
|
7
main.cpp
7
main.cpp
|
@ -1,6 +1,7 @@
|
|||
#include "Dialog/mainwindow.h"
|
||||
#include <DApplication>
|
||||
#include <DApplicationSettings>
|
||||
#include <DLog>
|
||||
#include <DMessageBox>
|
||||
#include <DTitlebar>
|
||||
#include <DWidgetUtil>
|
||||
|
@ -9,6 +10,7 @@
|
|||
#include <QTranslator>
|
||||
|
||||
DWIDGET_USE_NAMESPACE
|
||||
DCORE_USE_NAMESPACE
|
||||
|
||||
int main(int argc, char *argv[]) {
|
||||
//解决 root/ubuntu 主题样式走形
|
||||
|
@ -42,7 +44,7 @@ int main(int argc, char *argv[]) {
|
|||
|
||||
a.setOrganizationName("WingCloud");
|
||||
a.setApplicationName("WingGifEditor");
|
||||
a.setApplicationVersion("1.1.1");
|
||||
a.setApplicationVersion("1.1.2");
|
||||
a.setProductIcon(QIcon(":/images/icon.png"));
|
||||
a.setProductName(QObject::tr("WingGifEditor"));
|
||||
a.setApplicationDescription(
|
||||
|
@ -64,6 +66,9 @@ int main(int argc, char *argv[]) {
|
|||
DApplicationSettings as;
|
||||
Q_UNUSED(as)
|
||||
|
||||
DLogManager::registerFileAppender();
|
||||
DLogManager::registerConsoleAppender();
|
||||
|
||||
MainWindow w;
|
||||
|
||||
if (filename.length() && w.checkIsGif(filename))
|
||||
|
|
|
@ -65,7 +65,6 @@
|
|||
<file>images/readonly.png</file>
|
||||
<file>images/rwg.png</file>
|
||||
<file>images/fitinview.png</file>
|
||||
<file>settings.json</file>
|
||||
<file>images/setting.png</file>
|
||||
<file>images/fullscreen.png</file>
|
||||
</qresource>
|
||||
|
|
|
@ -1,12 +0,0 @@
|
|||
#include <DSettings>
|
||||
|
||||
void GenerateSettingTranslate() {
|
||||
auto group_editor = QObject::tr("Editor");
|
||||
auto group_appearance = QObject::tr("Appearance");
|
||||
|
||||
auto editor_image = QObject::tr("Image");
|
||||
auto image_quality = QObject::tr("Quality");
|
||||
|
||||
auto window = QObject::tr("Window");
|
||||
auto app_windowstate = QObject::tr("Window size");
|
||||
}
|
|
@ -1,54 +0,0 @@
|
|||
{
|
||||
"groups": [
|
||||
{
|
||||
"key": "editor",
|
||||
"name": "Editor",
|
||||
"groups": [
|
||||
{
|
||||
"key": "image",
|
||||
"name": "Image",
|
||||
"options": [
|
||||
{
|
||||
"key": "quality",
|
||||
"name": "Quality",
|
||||
"type": "spinbutton",
|
||||
"default": 10,
|
||||
"max": 30,
|
||||
"min": 1
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"key": "appearance",
|
||||
"name": "Appearance",
|
||||
"groups": [
|
||||
{
|
||||
"key": "window",
|
||||
"name": "Window",
|
||||
"options": [
|
||||
{
|
||||
"key": "windowsize",
|
||||
"name": "Window size",
|
||||
"type": "combobox",
|
||||
"default": "window_normal"
|
||||
},
|
||||
{
|
||||
"key": "window_width",
|
||||
"hide": true,
|
||||
"reset": false,
|
||||
"default": 0.6
|
||||
},
|
||||
{
|
||||
"key": "window_height",
|
||||
"hide": true,
|
||||
"reset": false,
|
||||
"default": 0.6
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
26
更新日志.log
26
更新日志.log
|
@ -1,8 +1,18 @@
|
|||
更新日志(由寂静的羽夏编写):
|
||||
|
||||
v1.0 beta:
|
||||
1. 实现基本编辑功能
|
||||
2. 实现洋葱皮功能 #I5J8WY
|
||||
v1.1.2:
|
||||
1. 更换编码库为 cgif
|
||||
2. 增加图像后处理,充分利用现有的控制台图像处理工具
|
||||
3. 修复切割图像窗体关闭后仍不恢复的 Bug
|
||||
4. 增加日志系统
|
||||
|
||||
v1.1.1:
|
||||
1. 使等待弹窗提示动画更加流畅
|
||||
2. 增大神经网络参数,以解决部分 GIF 图片无法正常保存的问题
|
||||
|
||||
v1.1.0:
|
||||
1. 修复已知问题,优化用户体验
|
||||
2. 修复等待弹窗提示被卡死的 Bug
|
||||
|
||||
v1.0.0:
|
||||
1. 修复从 GIF 创建应用闪退
|
||||
|
@ -10,10 +20,6 @@ v1.0.0:
|
|||
3. 修复部分已知 Bug
|
||||
4. 更换 GIF 编码解析库,摆脱额外依赖
|
||||
|
||||
v1.1.0:
|
||||
1. 修复已知问题,优化用户体验
|
||||
2. 修复等待弹窗提示被卡死的 Bug
|
||||
|
||||
v1.1.1:
|
||||
1. 使等待弹窗提示动画更加流畅
|
||||
2. 增大神经网络参数,以解决部分 GIF 图片无法正常保存的问题
|
||||
v1.0 beta:
|
||||
1. 实现基本编辑功能
|
||||
2. 实现洋葱皮功能 #I5J8WY
|
||||
|
|
Loading…
Reference in New Issue