build: split the engine into liboakengine.so; worker drops the UI entirely
Physical split: app/{audio,cli,codec,common,config,node,pluginSupport,
render,task,timeline,undo,tool,shaders} plus coreengine, version and
ui/icons+colorcoding move to a new top-level engine/ tree, built as
liboakengine.so (shared). The render backends (oakgl/oakvulkan) move
with it and link the engine library instead of embedding a static
render-core subset (libolive-rendercore is gone).
- oak-render-worker now links liboakengine instead of the whole
libolive-editor object set: 336MB -> 2.9MB, no Qt Widgets UI
- the editor links liboakengine for the engine and keeps only UI
objects in libolive-editor
- install/packaging: GNUInstallDirs libdir on Linux, bundle copy on
macOS, oakengine.dll staged for NSIS, AppImage validation entry
- fix backend lookup for the new layout: DynamicRenderer searched
../app but backends now live in engine/; a stale pre-split liboakgl
in the build tree got dlopened instead, re-initialized and later
destroyed the interposed engine statics (full-suite segfault at
DialogSequenceParameterTab, found via gdb watchpoint)
This commit is contained in:
@@ -0,0 +1,494 @@
|
||||
/*
|
||||
* Oak Video Editor - Non-Linear Video Editor
|
||||
* Copyright (C) 2025 Olive CE Team
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#include "html.h"
|
||||
|
||||
#include <QDebug>
|
||||
#include <QTextBlock>
|
||||
|
||||
#include "xmlutils.h"
|
||||
|
||||
namespace olive
|
||||
{
|
||||
|
||||
const QVector<QString> Html::k_block_tags = { QStringLiteral("p"),
|
||||
QStringLiteral("div") };
|
||||
|
||||
inline bool str_equals(const QStringView &a, const QStringView &b)
|
||||
{
|
||||
return !a.compare(b, Qt::CaseInsensitive);
|
||||
}
|
||||
|
||||
QString Html::doc_to_html(const QTextDocument *doc)
|
||||
{
|
||||
QString html;
|
||||
QXmlStreamWriter writer(&html);
|
||||
|
||||
//writer.setAutoFormatting(true);
|
||||
|
||||
for (auto it = doc->begin(); it != doc->end(); it = it.next()) {
|
||||
write_block(&writer, it);
|
||||
}
|
||||
|
||||
return html;
|
||||
}
|
||||
|
||||
struct HtmlNode {
|
||||
QString tag;
|
||||
QTextCharFormat format;
|
||||
};
|
||||
|
||||
QTextCharFormat merge_html_formats(const QVector<HtmlNode> &stack)
|
||||
{
|
||||
QTextCharFormat f;
|
||||
|
||||
for (int i = 0; i < stack.size(); i++) {
|
||||
f.merge(stack.at(i).format);
|
||||
}
|
||||
|
||||
return f;
|
||||
}
|
||||
|
||||
void Html::html_to_doc(QTextDocument *doc, const QString &html)
|
||||
{
|
||||
// Empty doc
|
||||
doc->clear();
|
||||
bool inside_block = true;
|
||||
|
||||
// Create cursor, which appears to be Qt's official way of inserting blocks and fragments
|
||||
QTextCursor c(doc);
|
||||
|
||||
QString wrapped = QStringLiteral("<html>").append(html).append("</html>");
|
||||
QXmlStreamReader reader(wrapped);
|
||||
|
||||
QVector<HtmlNode> fmt_stack;
|
||||
|
||||
QTextCharFormat default_fmt;
|
||||
default_fmt.setFontWeight(QFont::Normal);
|
||||
fmt_stack.append({ QStringLiteral("html"), default_fmt });
|
||||
|
||||
QTextCharFormat current_fmt;
|
||||
|
||||
while (!reader.atEnd()) {
|
||||
reader.readNext();
|
||||
|
||||
if (reader.tokenType() == QXmlStreamReader::StartElement) {
|
||||
QString tag = reader.name().toString().toLower();
|
||||
|
||||
fmt_stack.append({ tag, read_char_format(reader.attributes()) });
|
||||
current_fmt = merge_html_formats(fmt_stack);
|
||||
|
||||
if (k_block_tags.contains(tag)) {
|
||||
QTextBlockFormat block_fmt =
|
||||
read_block_format(reader.attributes());
|
||||
if (inside_block) {
|
||||
c.setBlockFormat(block_fmt);
|
||||
c.setBlockCharFormat(current_fmt);
|
||||
} else {
|
||||
c.insertBlock(block_fmt, current_fmt);
|
||||
inside_block = true;
|
||||
}
|
||||
}
|
||||
|
||||
} else if (reader.tokenType() == QXmlStreamReader::Characters) {
|
||||
QString characters = reader.text().toString();
|
||||
c.insertText(characters, current_fmt);
|
||||
|
||||
} else if (reader.tokenType() == QXmlStreamReader::EndElement) {
|
||||
QString tag = reader.name().toString().toLower();
|
||||
|
||||
for (int i = fmt_stack.size() - 1; i >= 0; i--) {
|
||||
if (fmt_stack.at(i).tag == tag) {
|
||||
fmt_stack.removeAt(i);
|
||||
current_fmt = merge_html_formats(fmt_stack);
|
||||
|
||||
if (k_block_tags.contains(tag)) {
|
||||
inside_block = false;
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (reader.error()) {
|
||||
qCritical() << "Failed to parse HTML:" << reader.errorString();
|
||||
}
|
||||
}
|
||||
|
||||
void Html::write_block(QXmlStreamWriter *writer, const QTextBlock &block)
|
||||
{
|
||||
writer->writeStartElement(QStringLiteral("p"));
|
||||
|
||||
const QTextBlockFormat &fmt = block.blockFormat();
|
||||
|
||||
// Write block alignment
|
||||
if (!(fmt.alignment() & Qt::AlignLeft)) {
|
||||
if (fmt.alignment() & Qt::AlignRight) {
|
||||
writer->writeAttribute(QStringLiteral("align"),
|
||||
QStringLiteral("right"));
|
||||
} else if (fmt.alignment() & Qt::AlignHCenter) {
|
||||
writer->writeAttribute(QStringLiteral("align"),
|
||||
QStringLiteral("center"));
|
||||
} else if (fmt.alignment() & Qt::AlignJustify) {
|
||||
writer->writeAttribute(QStringLiteral("align"),
|
||||
QStringLiteral("justify"));
|
||||
}
|
||||
}
|
||||
|
||||
// RTL support
|
||||
if (block.textDirection() == Qt::RightToLeft) {
|
||||
writer->writeAttribute(QStringLiteral("dir"), QStringLiteral("rtl"));
|
||||
}
|
||||
|
||||
// Write CSS attributes
|
||||
QString style;
|
||||
|
||||
if (fmt.lineHeightType() != QTextBlockFormat::SingleHeight) {
|
||||
write_css_property(&style, QStringLiteral("line-height"),
|
||||
QStringLiteral("%1%").arg(fmt.lineHeight()));
|
||||
}
|
||||
|
||||
write_char_format(&style, block.charFormat());
|
||||
|
||||
if (!style.isEmpty()) {
|
||||
writer->writeAttribute(QStringLiteral("style"), style);
|
||||
}
|
||||
|
||||
auto it = block.begin();
|
||||
|
||||
if (it != block.end()) {
|
||||
for (; it != block.end(); it++) {
|
||||
write_fragment(writer, it.fragment());
|
||||
}
|
||||
}
|
||||
|
||||
writer->writeEndElement(); // p
|
||||
}
|
||||
|
||||
void Html::write_fragment(QXmlStreamWriter *writer,
|
||||
const QTextFragment &fragment)
|
||||
{
|
||||
const QTextCharFormat &fmt = fragment.charFormat();
|
||||
|
||||
writer->writeStartElement(QStringLiteral("span"));
|
||||
|
||||
// Write CSS attributes
|
||||
QString style;
|
||||
|
||||
write_char_format(&style, fmt);
|
||||
|
||||
if (!style.isEmpty()) {
|
||||
writer->writeAttribute(QStringLiteral("style"), style);
|
||||
}
|
||||
|
||||
QStringList lines = fragment.text().split(QChar::LineSeparator);
|
||||
bool first_line = true;
|
||||
foreach (const QString &l, lines) {
|
||||
if (first_line) {
|
||||
first_line = false;
|
||||
} else {
|
||||
writer->writeEmptyElement(QStringLiteral("br"));
|
||||
}
|
||||
writer->writeCharacters(l);
|
||||
}
|
||||
|
||||
writer->writeEndElement(); // span
|
||||
}
|
||||
|
||||
void Html::write_css_property(QString *style, const QString &key,
|
||||
const QStringList &values)
|
||||
{
|
||||
QString value;
|
||||
foreach (QString v, values) {
|
||||
if (v.contains(' ')) {
|
||||
v = QStringLiteral("'%1'").arg(v);
|
||||
}
|
||||
|
||||
append_string_auto_space(&value, v);
|
||||
}
|
||||
|
||||
append_string_auto_space(style, QStringLiteral("%1: %2;").arg(key, value));
|
||||
}
|
||||
|
||||
void Html::write_char_format(QString *style, const QTextCharFormat &fmt)
|
||||
{
|
||||
QStringList families = fmt.fontFamilies().toStringList();
|
||||
if (!families.isEmpty()) {
|
||||
write_css_property(style, QStringLiteral("font-family"),
|
||||
families.first());
|
||||
}
|
||||
|
||||
if (fmt.hasProperty(QTextFormat::FontPointSize)) {
|
||||
write_css_property(
|
||||
style, QStringLiteral("font-size"),
|
||||
QStringLiteral("%1pt").arg(QString::number(fmt.fontPointSize())));
|
||||
}
|
||||
|
||||
if (fmt.hasProperty(QTextFormat::FontWeight)) {
|
||||
write_css_property(style, QStringLiteral("font-weight"),
|
||||
QString::number(fmt.fontWeight() * 8));
|
||||
}
|
||||
|
||||
if (fmt.hasProperty(QTextFormat::FontItalic)) {
|
||||
write_css_property(style, QStringLiteral("font-style"),
|
||||
fmt.fontItalic() ? QStringLiteral("italic") :
|
||||
QStringLiteral("normal"));
|
||||
}
|
||||
|
||||
if (fmt.hasProperty(QTextFormat::FontStyleName)) {
|
||||
write_css_property(style, QStringLiteral("-ove-font-style"),
|
||||
fmt.fontStyleName().toString());
|
||||
}
|
||||
|
||||
QStringList deco;
|
||||
|
||||
if (fmt.fontUnderline()) {
|
||||
deco.append(QStringLiteral("underline"));
|
||||
}
|
||||
|
||||
if (fmt.fontStrikeOut()) {
|
||||
deco.append(QStringLiteral("line-through"));
|
||||
}
|
||||
|
||||
if (fmt.fontOverline()) {
|
||||
deco.append(QStringLiteral("overline"));
|
||||
}
|
||||
|
||||
if (!deco.isEmpty()) {
|
||||
write_css_property(style, QStringLiteral("text-decoration"), deco);
|
||||
}
|
||||
|
||||
if (fmt.foreground().style() != Qt::NoBrush) {
|
||||
const QColor &color = fmt.foreground().color();
|
||||
QString cs;
|
||||
|
||||
if (color.alpha() == 255) {
|
||||
cs = color.name();
|
||||
} else if (color.alpha()) {
|
||||
cs = QStringLiteral("rgba(%1, %2, %3, %4)")
|
||||
.arg(QString::number(color.red()),
|
||||
QString::number(color.green()),
|
||||
QString::number(color.blue()),
|
||||
QString::number(color.alphaF()));
|
||||
}
|
||||
|
||||
write_css_property(style, QStringLiteral("color"), cs);
|
||||
}
|
||||
|
||||
if (fmt.fontCapitalization() != QFont::MixedCase) {
|
||||
if (fmt.fontCapitalization() == QFont::SmallCaps) {
|
||||
write_css_property(style, QStringLiteral("font-variant"),
|
||||
QStringLiteral("small-caps"));
|
||||
// TODO: Add others
|
||||
}
|
||||
}
|
||||
|
||||
if (fmt.fontLetterSpacing() != 0.0) {
|
||||
write_css_property(style, QStringLiteral("letter-spacing"),
|
||||
QStringLiteral("%1%").arg(
|
||||
QString::number(fmt.fontLetterSpacing())));
|
||||
}
|
||||
|
||||
if (fmt.fontStretch() != 0) {
|
||||
write_css_property(
|
||||
style, QStringLiteral("font-stretch"),
|
||||
QStringLiteral("%1%").arg(QString::number(fmt.fontStretch())));
|
||||
}
|
||||
}
|
||||
|
||||
QTextCharFormat Html::read_char_format(const QXmlStreamAttributes &attributes)
|
||||
{
|
||||
QTextCharFormat fmt;
|
||||
|
||||
foreach (const QXmlStreamAttribute &attr, attributes) {
|
||||
if (str_equals(attr.name(), QStringLiteral("style"))) {
|
||||
auto css = get_css_from_style(attr.value().toString());
|
||||
|
||||
for (auto it = css.begin(); it != css.end(); it++) {
|
||||
const QString &first_val = it.value().first();
|
||||
|
||||
if (it.key() == QStringLiteral("font-family")) {
|
||||
fmt.setFontFamilies({ first_val });
|
||||
} else if (it.key() == QStringLiteral("font-size")) {
|
||||
if (first_val.endsWith(QStringLiteral("pt"),
|
||||
Qt::CaseInsensitive)) {
|
||||
fmt.setFontPointSize(first_val.chopped(2).toDouble());
|
||||
}
|
||||
} else if (it.key() == QStringLiteral("font-weight")) {
|
||||
fmt.setFontWeight(first_val.toInt() / 8);
|
||||
} else if (it.key() == QStringLiteral("font-style")) {
|
||||
fmt.setFontItalic(
|
||||
str_equals(first_val, QStringLiteral("italic")));
|
||||
} else if (it.key() == QStringLiteral("text-decoration")) {
|
||||
foreach (const QString &v, it.value()) {
|
||||
if (str_equals(v, QStringLiteral("underline"))) {
|
||||
fmt.setFontUnderline(true);
|
||||
} else if (str_equals(v,
|
||||
QStringLiteral("line-through"))) {
|
||||
fmt.setFontStrikeOut(true);
|
||||
} else if (str_equals(v, QStringLiteral("overline"))) {
|
||||
fmt.setFontOverline(true);
|
||||
}
|
||||
}
|
||||
} else if (it.key() == QStringLiteral("color")) {
|
||||
if (first_val.startsWith(QStringLiteral("rgba"),
|
||||
Qt::CaseInsensitive)) {
|
||||
QString vals_only = first_val;
|
||||
vals_only.remove(QStringLiteral("rgba"));
|
||||
vals_only.remove(QStringLiteral("("));
|
||||
vals_only.remove(QStringLiteral(")"));
|
||||
QStringList rgba = vals_only.split(',');
|
||||
if (rgba.size() == 4) {
|
||||
QColor c;
|
||||
c.setRed(rgba.at(0).toInt()); // Writer emits 0-255 RGB (CSS rgba() convention)
|
||||
c.setGreen(rgba.at(1).toInt());
|
||||
c.setBlue(rgba.at(2).toInt());
|
||||
c.setAlphaF(rgba.at(3).toDouble());
|
||||
fmt.setForeground(c);
|
||||
}
|
||||
} else {
|
||||
fmt.setForeground(QColor(first_val));
|
||||
}
|
||||
} else if (it.key() == QStringLiteral("font-variant")) {
|
||||
if (str_equals(first_val, QStringLiteral("small-caps"))) {
|
||||
fmt.setFontCapitalization(QFont::SmallCaps);
|
||||
}
|
||||
} else if (it.key() == QStringLiteral("letter-spacing")) {
|
||||
if (first_val.contains(QChar('%'))) {
|
||||
fmt.setFontLetterSpacing(
|
||||
first_val.chopped(1).toDouble());
|
||||
}
|
||||
} else if (it.key() == QStringLiteral("font-stretch")) {
|
||||
if (first_val.contains(QChar('%'))) {
|
||||
fmt.setFontStretch(first_val.chopped(1).toInt());
|
||||
}
|
||||
} else if (it.key() == QStringLiteral("-ove-font-style")) {
|
||||
fmt.setFontStyleName(first_val);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return fmt;
|
||||
}
|
||||
|
||||
QTextBlockFormat Html::read_block_format(const QXmlStreamAttributes &attributes)
|
||||
{
|
||||
QTextBlockFormat block_fmt;
|
||||
|
||||
foreach (const QXmlStreamAttribute &attr, attributes) {
|
||||
if (str_equals(attr.name(), QStringLiteral("align"))) {
|
||||
if (str_equals(attr.value(), QStringLiteral("right"))) {
|
||||
block_fmt.setAlignment(Qt::AlignRight);
|
||||
} else if (str_equals(attr.value(), QStringLiteral("center"))) {
|
||||
block_fmt.setAlignment(Qt::AlignHCenter);
|
||||
} else if (str_equals(attr.value(), QStringLiteral("justify"))) {
|
||||
block_fmt.setAlignment(Qt::AlignJustify);
|
||||
}
|
||||
} else if (str_equals(attr.name(), QStringLiteral("dir"))) {
|
||||
if (str_equals(attr.value(), QStringLiteral("rtl"))) {
|
||||
block_fmt.setLayoutDirection(Qt::RightToLeft);
|
||||
}
|
||||
} else if (str_equals(attr.name(), QStringLiteral("style"))) {
|
||||
auto css = get_css_from_style(attr.value().toString());
|
||||
|
||||
for (auto it = css.begin(); it != css.end(); it++) {
|
||||
if (it.key() == QStringLiteral("line-height")) {
|
||||
const QString &first_val = it.value().constFirst();
|
||||
if (first_val.contains(QChar('%'))) {
|
||||
block_fmt.setLineHeight(
|
||||
first_val.chopped(1).toDouble(),
|
||||
QTextBlockFormat::ProportionalHeight);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return block_fmt;
|
||||
}
|
||||
|
||||
void Html::append_string_auto_space(QString *s, const QString &append)
|
||||
{
|
||||
if (!s->isEmpty()) {
|
||||
s->append(QChar(' '));
|
||||
}
|
||||
|
||||
s->append(append);
|
||||
}
|
||||
|
||||
QMap<QString, QStringList> Html::get_css_from_style(const QString &s)
|
||||
{
|
||||
QMap<QString, QStringList> map;
|
||||
|
||||
QStringList list = s.split(QChar(';'));
|
||||
|
||||
foreach (const QString &a, list) {
|
||||
QStringList kv = a.split(QChar(':'));
|
||||
|
||||
if (kv.size() != 2) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// I'm sure there's regex that could do this, but I couldn't figure it out. It needs to split
|
||||
// by space EXCEPT within quotes OR double-quotes, and said quotes should be EXCLUDED from each
|
||||
// match. Also commas should be filtered out.
|
||||
QStringList values;
|
||||
const QString &val = kv.at(1);
|
||||
QChar in_quote(0);
|
||||
QString current_str;
|
||||
for (int i = 0; i < val.size(); i++) {
|
||||
const QChar ¤t_char = val.at(i);
|
||||
|
||||
if (!in_quote.isNull()) {
|
||||
// If inside quotes and character isn't quote, indiscriminately append char
|
||||
if (current_char == in_quote) {
|
||||
in_quote = QChar(0);
|
||||
} else {
|
||||
current_str.append(current_char);
|
||||
}
|
||||
} else if (current_char.isSpace() || current_char == QChar(',')) {
|
||||
// Dump current
|
||||
if (!current_str.isEmpty()) {
|
||||
values.append(current_str);
|
||||
current_str.clear();
|
||||
}
|
||||
} else if (in_quote.isNull() && (current_char == QChar('\'') ||
|
||||
current_char == QChar('"'))) {
|
||||
in_quote = current_char;
|
||||
} else {
|
||||
current_str.append(current_char);
|
||||
}
|
||||
}
|
||||
|
||||
if (!current_str.isEmpty()) {
|
||||
values.append(current_str);
|
||||
}
|
||||
|
||||
// Not sure if this will ever happen, but just in case, we will avoid assert failures with this
|
||||
if (values.isEmpty()) {
|
||||
values.append(QString());
|
||||
}
|
||||
|
||||
map[kv.at(0).trimmed().toLower()] = values;
|
||||
}
|
||||
|
||||
return map;
|
||||
}
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user