übernahme Code Shortcut
This commit is contained in:
416
src/htmlgenerator.cpp
Normal file
416
src/htmlgenerator.cpp
Normal file
@@ -0,0 +1,416 @@
|
||||
/*
|
||||
* Copyright (c) 2025 Meltytech, LLC
|
||||
*
|
||||
* 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 "htmlgenerator.h"
|
||||
#include "Logger.h"
|
||||
#include "settings.h"
|
||||
|
||||
#include <QDir>
|
||||
#include <QFile>
|
||||
#include <QJsonArray>
|
||||
#include <QJsonDocument>
|
||||
#include <QNetworkReply>
|
||||
#include <QUrl>
|
||||
|
||||
HtmlGenerator::HtmlGenerator(QObject *parent)
|
||||
: QObject(parent)
|
||||
, m_webSocket(new QWebSocket(QString(), QWebSocketProtocol::VersionLatest, this))
|
||||
, m_networkManager(new QNetworkAccessManager(this))
|
||||
, m_messageId(1)
|
||||
, m_chromeProcess(new QProcess(this))
|
||||
{
|
||||
connect(m_webSocket, &QWebSocket::connected, this, &HtmlGenerator::onWebSocketConnected);
|
||||
connect(m_webSocket, &QWebSocket::textMessageReceived, this, &HtmlGenerator::onMessageReceived);
|
||||
connect(m_webSocket, &QWebSocket::disconnected, this, &HtmlGenerator::onWebSocketDisconnected);
|
||||
}
|
||||
|
||||
HtmlGenerator::~HtmlGenerator()
|
||||
{
|
||||
if (m_chromeProcess && m_chromeProcess->state() == QProcess::Running) {
|
||||
m_chromeProcess->terminate();
|
||||
m_chromeProcess->waitForFinished(2000);
|
||||
m_chromeProcess->kill();
|
||||
}
|
||||
}
|
||||
|
||||
void HtmlGenerator::setAnimationParameters(double fps, int duration)
|
||||
{
|
||||
m_fps = fps;
|
||||
m_duration = duration;
|
||||
m_animationMode = (fps > 0 && duration > 0);
|
||||
}
|
||||
|
||||
void HtmlGenerator::launchBrowser(const QString &executablePath,
|
||||
const QString &url,
|
||||
const QSize &viewport,
|
||||
const QString &outputPath)
|
||||
{
|
||||
m_url = url;
|
||||
m_viewport = viewport;
|
||||
m_outputPath = outputPath;
|
||||
|
||||
// Start browser with appropriate arguments
|
||||
QStringList arguments;
|
||||
arguments << "--remote-debugging-port=9222"
|
||||
<< "--headless=new"
|
||||
<< "--disable-gpu"
|
||||
<< "--no-sandbox"
|
||||
<< "--no-zygote"
|
||||
// << "--single-process"
|
||||
<< "--no-first-run"
|
||||
<< "--no-default-browser-check"
|
||||
<< "--allow-file-access-from-files"
|
||||
<< "--user-data-dir=" + m_tempDir.path();
|
||||
|
||||
connect(m_chromeProcess, &QProcess::finished, this, &HtmlGenerator::onChromeProcessFinished);
|
||||
connect(m_chromeProcess, &QProcess::errorOccurred, this, &HtmlGenerator::onChromeProcessError);
|
||||
|
||||
#if defined(Q_OS_UNIX) && !defined(Q_OS_MAC)
|
||||
auto env = QProcessEnvironment::systemEnvironment();
|
||||
env.remove("LD_LIBRARY_PATH");
|
||||
LOG_DEBUG() << env.toStringList();
|
||||
m_chromeProcess->setProcessEnvironment(env);
|
||||
#endif
|
||||
|
||||
LOG_DEBUG() << executablePath + " " + arguments.join(' ');
|
||||
m_chromeProcess->start(executablePath, arguments);
|
||||
|
||||
if (!m_chromeProcess->waitForStarted(5000)) {
|
||||
LOG_ERROR() << "Failed to start browser process";
|
||||
Settings.setChromiumPath(QString());
|
||||
return;
|
||||
}
|
||||
|
||||
// Wait a bit for browser to start up
|
||||
QTimer::singleShot(2000, this, &HtmlGenerator::connectToBrowser);
|
||||
}
|
||||
|
||||
void HtmlGenerator::connectToBrowser()
|
||||
{
|
||||
// Get the list of pages from Chrome
|
||||
QNetworkRequest request(QUrl("http://localhost:9222/json/list"));
|
||||
QNetworkReply *reply = m_networkManager->get(request);
|
||||
|
||||
connect(reply, &QNetworkReply::finished, this, [this, reply]() {
|
||||
if (reply->error() != QNetworkReply::NoError) {
|
||||
LOG_ERROR() << "Failed to get Chrome debug info:" << reply->errorString();
|
||||
Settings.setChromiumPath(QString());
|
||||
return;
|
||||
}
|
||||
|
||||
QJsonDocument doc = QJsonDocument::fromJson(reply->readAll());
|
||||
QJsonArray pages = doc.array();
|
||||
|
||||
QString webSocketUrl;
|
||||
// Look for an existing page or create a new one
|
||||
for (auto pageValue : std::as_const(pages)) {
|
||||
const auto page = pageValue.toObject();
|
||||
if (page["type"].toString() == "page") {
|
||||
webSocketUrl = page["webSocketDebuggerUrl"].toString();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (webSocketUrl.isEmpty()) {
|
||||
// Create a new page
|
||||
LOG_DEBUG() << "No existing page found, creating a new one";
|
||||
createNewPage();
|
||||
} else {
|
||||
LOG_DEBUG() << "Using existing page, connecting to WebSocket:" << webSocketUrl;
|
||||
m_webSocket->open(QUrl(webSocketUrl));
|
||||
}
|
||||
reply->deleteLater();
|
||||
});
|
||||
}
|
||||
|
||||
void HtmlGenerator::createNewPage()
|
||||
{
|
||||
QNetworkRequest request(QUrl("http://localhost:9222/json/new"));
|
||||
auto *reply = m_networkManager->get(request);
|
||||
|
||||
connect(reply, &QNetworkReply::finished, this, [this, reply]() {
|
||||
if (reply->error() != QNetworkReply::NoError) {
|
||||
LOG_ERROR() << "Failed to create new page:" << reply->errorString();
|
||||
return;
|
||||
}
|
||||
|
||||
const auto doc = QJsonDocument::fromJson(reply->readAll());
|
||||
const auto page = doc.object();
|
||||
const auto webSocketUrl = page["webSocketDebuggerUrl"].toString();
|
||||
|
||||
if (webSocketUrl.isEmpty()) {
|
||||
LOG_ERROR() << "No WebSocket URL found in new page response";
|
||||
return;
|
||||
}
|
||||
|
||||
LOG_DEBUG() << "Created new page, connecting to WebSocket:" << webSocketUrl;
|
||||
m_webSocket->open(QUrl(webSocketUrl));
|
||||
reply->deleteLater();
|
||||
});
|
||||
}
|
||||
|
||||
void HtmlGenerator::onWebSocketConnected()
|
||||
{
|
||||
LOG_DEBUG() << "Connected to Chrome DevTools";
|
||||
|
||||
// Enable Runtime and Page events
|
||||
sendCommand("Runtime.enable");
|
||||
sendCommand("Page.enable");
|
||||
|
||||
// Set default background color to transparent
|
||||
QJsonObject params;
|
||||
params["color"] = QJsonObject{{"r", 0}, {"g", 0}, {"b", 0}, {"a", 0}};
|
||||
sendCommand("Emulation.setDefaultBackgroundColorOverride", params);
|
||||
|
||||
// Set viewport
|
||||
params = QJsonObject();
|
||||
params["width"] = m_viewport.width();
|
||||
params["height"] = m_viewport.height();
|
||||
params["deviceScaleFactor"] = 1;
|
||||
params["mobile"] = false;
|
||||
sendCommand("Emulation.setDeviceMetricsOverride", params);
|
||||
|
||||
// Navigate to the URL
|
||||
QJsonObject navParams;
|
||||
navParams["url"] = m_url;
|
||||
sendCommand("Page.navigate", navParams);
|
||||
|
||||
// Set a timeout for taking screenshot in case page load event doesn't fire
|
||||
QTimer::singleShot(10000, this, &HtmlGenerator::takeScreenshot);
|
||||
}
|
||||
|
||||
void HtmlGenerator::onMessageReceived(const QString &message)
|
||||
{
|
||||
// if (!message.contains("\"data\""))
|
||||
// LOG_DEBUG() << "Received message:" << message;
|
||||
|
||||
const auto doc = QJsonDocument::fromJson(message.toUtf8());
|
||||
const auto obj = doc.object();
|
||||
|
||||
if (obj.contains("method")) {
|
||||
const auto method = obj["method"].toString();
|
||||
if (method == "Page.loadEventFired") {
|
||||
LOG_DEBUG() << "Page loaded, starting capture";
|
||||
if (m_animationMode) {
|
||||
startAnimationCapture();
|
||||
} else {
|
||||
takeScreenshot();
|
||||
}
|
||||
}
|
||||
} else if (obj.contains("id") && obj.contains("result")) {
|
||||
const auto id = obj["id"].toInt();
|
||||
if (m_pendingScreenshot && id == m_screenshotMessageId) {
|
||||
if (m_animationMode) {
|
||||
handleAnimationFrame(obj["result"].toObject());
|
||||
} else {
|
||||
handleScreenshotResult(obj["result"].toObject());
|
||||
}
|
||||
}
|
||||
} else if (obj.contains("error")) {
|
||||
LOG_ERROR() << "Error received:" << QJsonDocument(obj["error"].toObject()).toJson();
|
||||
}
|
||||
}
|
||||
|
||||
void HtmlGenerator::onWebSocketDisconnected()
|
||||
{
|
||||
LOG_DEBUG() << "Disconnected from Chrome DevTools";
|
||||
}
|
||||
|
||||
void HtmlGenerator::onChromeProcessFinished(int exitCode, QProcess::ExitStatus exitStatus)
|
||||
{
|
||||
// Q_UNUSED(exitCode)
|
||||
Q_UNUSED(exitStatus)
|
||||
LOG_DEBUG() << "Chrome process finished" << exitCode;
|
||||
}
|
||||
|
||||
void HtmlGenerator::onChromeProcessError(QProcess::ProcessError error)
|
||||
{
|
||||
// Ignore crash errors after successful screenshot completion
|
||||
if (error == QProcess::Crashed && m_screenshotCompleted) {
|
||||
LOG_DEBUG() << "Chrome process crashed after successful screenshot completion (expected)";
|
||||
return;
|
||||
}
|
||||
|
||||
LOG_ERROR() << "Chrome process error:" << error;
|
||||
}
|
||||
|
||||
void HtmlGenerator::startAnimationCapture()
|
||||
{
|
||||
// Create output directory
|
||||
const QDir outputDir(m_outputPath);
|
||||
if (!outputDir.exists()) {
|
||||
if (!outputDir.mkpath(".")) {
|
||||
LOG_ERROR() << "Failed to create output directory:" << m_outputPath;
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Calculate animation parameters
|
||||
const auto frameInterval = 1000.0 / m_fps; // milliseconds per frame
|
||||
m_totalFrames = static_cast<int>(std::ceil(m_duration / frameInterval));
|
||||
m_currentFrame = 0;
|
||||
|
||||
LOG_DEBUG() << QString("Capturing %1 frames at %2 fps over %3ms...")
|
||||
.arg(m_totalFrames)
|
||||
.arg(m_fps)
|
||||
.arg(m_duration);
|
||||
|
||||
// Start timing
|
||||
m_animationElapsed.start();
|
||||
|
||||
// Take first frame
|
||||
captureAnimationFrame();
|
||||
}
|
||||
|
||||
void HtmlGenerator::captureAnimationFrame()
|
||||
{
|
||||
if (m_currentFrame >= m_totalFrames) {
|
||||
completeAnimationCapture();
|
||||
return;
|
||||
}
|
||||
|
||||
// LOG_DEBUG() << "Taking screenshot...";
|
||||
m_pendingScreenshot = true;
|
||||
|
||||
QJsonObject params;
|
||||
params["format"] = "png";
|
||||
params["omitBackground"] = true;
|
||||
params["captureBeyondViewport"] = false;
|
||||
m_screenshotMessageId = sendCommand("Page.captureScreenshot", params);
|
||||
}
|
||||
|
||||
void HtmlGenerator::handleAnimationFrame(const QJsonObject &result)
|
||||
{
|
||||
m_pendingScreenshot = false;
|
||||
|
||||
const auto base64Data = result["data"].toString();
|
||||
const auto imageData = QByteArray::fromBase64(base64Data.toUtf8());
|
||||
|
||||
// Save frame with zero-padded filename
|
||||
const auto frameNumber = QString("%1").arg(m_currentFrame, 4, 10, QLatin1Char('0'));
|
||||
const auto filename = QDir(m_outputPath).filePath(QString("frame_%1.png").arg(frameNumber));
|
||||
|
||||
QFile file(filename);
|
||||
if (file.open(QIODevice::WriteOnly)) {
|
||||
file.write(imageData);
|
||||
file.close();
|
||||
LOG_DEBUG() << "Captured frame" << (m_currentFrame + 1) << "/" << m_totalFrames;
|
||||
emit progressUpdate(float(m_currentFrame + 1) / m_totalFrames);
|
||||
} else {
|
||||
LOG_ERROR() << "Failed to save frame:" << file.errorString();
|
||||
return;
|
||||
}
|
||||
|
||||
m_currentFrame++;
|
||||
|
||||
// Schedule next frame
|
||||
if (m_currentFrame < m_totalFrames) {
|
||||
// Calculate when the next frame should be captured
|
||||
const auto frameInterval = 1000.0 / m_fps;
|
||||
const auto targetTime = static_cast<qint64>(m_currentFrame * frameInterval);
|
||||
const auto currentTime = m_animationElapsed.elapsed();
|
||||
|
||||
int delay = std::max(0LL, targetTime - currentTime);
|
||||
if (delay == 0)
|
||||
LOG_DEBUG() << "frame duration" << frameInterval << "delay" << targetTime - currentTime
|
||||
<< "ms";
|
||||
QTimer::singleShot(delay, this, &HtmlGenerator::captureAnimationFrame);
|
||||
} else {
|
||||
completeAnimationCapture();
|
||||
}
|
||||
}
|
||||
|
||||
void HtmlGenerator::completeAnimationCapture()
|
||||
{
|
||||
m_screenshotCompleted = true;
|
||||
|
||||
LOG_DEBUG() << QString("Animation frames saved to %1/").arg(m_outputPath);
|
||||
|
||||
// Close the browser and exit
|
||||
m_webSocket->close();
|
||||
if (m_chromeProcess && m_chromeProcess->state() == QProcess::Running) {
|
||||
m_chromeProcess->terminate();
|
||||
}
|
||||
|
||||
// Emit signal indicating animation frames are ready
|
||||
emit imageReady(m_outputPath);
|
||||
}
|
||||
|
||||
void HtmlGenerator::takeScreenshot()
|
||||
{
|
||||
if (m_pendingScreenshot) {
|
||||
LOG_DEBUG() << "Screenshot already pending, skipping";
|
||||
return;
|
||||
}
|
||||
|
||||
LOG_DEBUG() << "Taking screenshot...";
|
||||
m_pendingScreenshot = true;
|
||||
|
||||
QJsonObject params;
|
||||
params["format"] = "png";
|
||||
params["omitBackground"] = true;
|
||||
|
||||
m_screenshotMessageId = sendCommand("Page.captureScreenshot", params);
|
||||
}
|
||||
|
||||
void HtmlGenerator::handleScreenshotResult(const QJsonObject &result)
|
||||
{
|
||||
m_pendingScreenshot = false;
|
||||
m_screenshotCompleted = true;
|
||||
|
||||
const auto base64Data = result["data"].toString();
|
||||
const auto imageData = QByteArray::fromBase64(base64Data.toUtf8());
|
||||
bool success = false;
|
||||
|
||||
QFile file(m_outputPath);
|
||||
if (file.open(QIODevice::WriteOnly)) {
|
||||
file.write(imageData);
|
||||
file.close();
|
||||
success = true;
|
||||
LOG_DEBUG() << "Screenshot saved to:" << m_outputPath;
|
||||
} else {
|
||||
LOG_ERROR() << "Failed to save screenshot:" << file.errorString();
|
||||
}
|
||||
|
||||
// Close the browser and exit
|
||||
sendCommand("Browser.close");
|
||||
QTimer::singleShot(100, m_chromeProcess, [=]() {
|
||||
m_webSocket->close();
|
||||
if (m_chromeProcess && m_chromeProcess->state() == QProcess::Running)
|
||||
m_chromeProcess->terminate();
|
||||
});
|
||||
if (success)
|
||||
emit imageReady(m_outputPath);
|
||||
}
|
||||
|
||||
int HtmlGenerator::sendCommand(const QString &method, const QJsonObject ¶ms)
|
||||
{
|
||||
QJsonObject command;
|
||||
command["id"] = m_messageId;
|
||||
command["method"] = method;
|
||||
if (!params.isEmpty()) {
|
||||
command["params"] = params;
|
||||
}
|
||||
|
||||
const QJsonDocument doc(command);
|
||||
const QString message = doc.toJson(QJsonDocument::Compact);
|
||||
|
||||
// LOG_DEBUG() << "Sending command:" << message;
|
||||
m_webSocket->sendTextMessage(message);
|
||||
|
||||
return m_messageId++;
|
||||
}
|
||||
Reference in New Issue
Block a user