Skip to content

Commit 2ba4e19

Browse files
authored
Add PAG export support in viewer: PNG sequence, single frame PNG and APNG formats (#2755)
1 parent 51a3338 commit 2ba4e19

35 files changed

+3336
-44
lines changed

viewer/CMakeLists.txt

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -105,9 +105,13 @@ add_subdirectory(${CMAKE_CURRENT_SOURCE_DIR}/../ ./libpag)
105105
add_executable(PAGViewer ${RC_FILES} ${PAG_VIEWER_SOURCE_FILES} ${QT_RESOURCES})
106106
if (APPLE)
107107
list(APPEND PAG_VIEWER_INCLUDES ../third_party/out/rttr/mac/include)
108+
list(APPEND PAG_VIEWER_INCLUDES ../third_party/tgfx/third_party/out/zlib/mac/include)
109+
list(APPEND PAG_VIEWER_INCLUDES ../third_party/tgfx/third_party/out/libpng/mac/include)
108110
list(APPEND VIEWER_VENDOR_LIBRARIES ${CMAKE_SOURCE_DIR}/../vendor/sparkle/mac/Sparkle.framework)
109111
elseif (WIN32)
110112
list(APPEND PAG_VIEWER_INCLUDES ../third_party/out/rttr/win/include)
113+
list(APPEND PAG_VIEWER_INCLUDES ../third_party/tgfx/third_party/out/zlib/win/include)
114+
list(APPEND PAG_VIEWER_INCLUDES ../third_party/tgfx/third_party/out/libpng/win/include)
111115
list(APPEND PAG_VIEWER_INCLUDES ../vendor/winsparkle/include)
112116
list(APPEND VIEWER_VENDOR_LIBRARIES ${CMAKE_SOURCE_DIR}/../vendor/winsparkle/win/x64/${CMAKE_BUILD_TYPE}/WinSparkle.lib)
113117
endif ()

viewer/qml/Main.qml

Lines changed: 200 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,9 @@ import PAG
22
import QtCore
33
import QtQuick
44
import QtQuick.Dialogs
5+
import QtQuick.Controls
56
import Qt.labs.settings
7+
import Qt.labs.platform as Platform
68
import "components"
79
import "utils"
810

@@ -127,18 +129,6 @@ PAGWindow {
127129
}
128130
}
129131

130-
FileDialog {
131-
id: openPAGFileDialog
132-
visible: false
133-
title: qsTr("Open PAG File")
134-
fileMode: FileDialog.OpenFile
135-
nameFilters: ["PAG files(*.pag)"]
136-
onAccepted: {
137-
let filePath = openPAGFileDialog.selectedFile;
138-
mainForm.pagView.setFile(filePath);
139-
}
140-
}
141-
142132
SettingsWindow {
143133
id: settingsWindow
144134
visible: false
@@ -163,6 +153,120 @@ PAGWindow {
163153
aboutMessage: "<b>PAGViewer</b> " + Qt.application.version + "<br><br>Copyright © 2017-present Tencent. All rights reserved."
164154
}
165155

156+
PAGTaskFactory {
157+
id: taskFactory
158+
objectName: "taskFactory"
159+
}
160+
161+
FileDialog {
162+
id: openFileDialog
163+
164+
property var currentAcceptHandler: null
165+
166+
visible: false
167+
title: ""
168+
fileMode: FileDialog.OpenFile
169+
nameFilters: []
170+
}
171+
172+
Platform.FolderDialog {
173+
id: openFolderDialog
174+
175+
property var currentAcceptHandler: null
176+
177+
visible: false
178+
title: qsTr("Select Save Path")
179+
}
180+
181+
PAGWindow {
182+
id: progressWindow
183+
184+
property var task
185+
property alias progressBar: progressBar
186+
187+
width: 300
188+
height: 64
189+
minimumWidth: width
190+
maximumWidth: width
191+
minimumHeight: height
192+
maximumHeight: height
193+
hasMenu: false
194+
canResize: false
195+
titleBarHeight: windowTitleBarHeight
196+
visible: false
197+
198+
PAGRectangle {
199+
id: rectangle
200+
201+
color: "#2D2D37"
202+
anchors.fill: parent
203+
leftTopRadius: false
204+
rightTopRadius: false
205+
radius: 5
206+
207+
ProgressBar {
208+
id: progressBar
209+
width: parent.width - 24
210+
height: 30
211+
anchors.verticalCenter: parent.verticalCenter
212+
anchors.horizontalCenter: parent.horizontalCenter
213+
value: 0
214+
215+
contentItem: Item {
216+
Rectangle {
217+
width: parent.width
218+
height: 15
219+
radius: 5
220+
color: "#DDDDDD"
221+
anchors.verticalCenter: parent.verticalCenter
222+
}
223+
224+
Rectangle {
225+
width: progressBar.visualPosition * parent.width
226+
height: 15
227+
radius: 5
228+
color: "#448EF9"
229+
anchors.verticalCenter: parent.verticalCenter
230+
}
231+
232+
Text {
233+
anchors.centerIn: parent
234+
text: Math.round(progressBar.value * 100) + "%"
235+
color: progressBar.value > 0.5 ? "white" : "black"
236+
font.pixelSize: 12
237+
}
238+
}
239+
}
240+
}
241+
242+
onClosing: {
243+
if (task) {
244+
task.stop();
245+
}
246+
}
247+
}
248+
249+
Connections {
250+
id: taskConnections
251+
onProgressChanged: function (progress) {
252+
progressWindow.progressBar.value = progress;
253+
}
254+
255+
onVisibleChanged: function (visible) {
256+
progressWindow.visible = visible;
257+
}
258+
259+
onTaskFinished: function (filePath, result) {
260+
if (result !== 0) {
261+
let errStr = qsTr("Export failed, error code: ");
262+
alert(errStr + result);
263+
}
264+
progressWindow.task = null;
265+
progressWindow.progressBar.value = 0;
266+
progressWindow.visible = false;
267+
}
268+
}
269+
166270
Component.onCompleted: {
167271
viewWindow.title = "PAGViewer";
168272

@@ -217,11 +321,19 @@ PAGWindow {
217321
case "open-pag-file":
218322
if (mainForm.hasPAGFile) {
219323
let filePath = mainForm.pagView.filePath;
220-
openPAGFileDialog.currentFolder = Utils.getFileDir(filePath);
324+
openFileDialog.currentFolder = Utils.getFileDir(filePath);
221325
} else {
222-
openPAGFileDialog.currentFolder = StandardPaths.writableLocation(StandardPaths.DocumentsLocation);
326+
openFileDialog.currentFolder = StandardPaths.writableLocation(StandardPaths.DocumentsLocation);
223327
}
224-
openPAGFileDialog.open();
328+
openFileDialog.accepted.disconnect();
329+
openFileDialog.fileMode = FileDialog.OpenFile;
330+
openFileDialog.title = qsTr("Open PAG File");
331+
openFileDialog.nameFilters = ["PAG files(*.pag)"];
332+
openFileDialog.accepted.connect(function () {
333+
let filePath = openFileDialog.selectedFile;
334+
mainForm.pagView.setFile(filePath);
335+
});
336+
openFileDialog.open();
225337
break;
226338
case "close-window":
227339
viewWindow.close();
@@ -273,6 +385,79 @@ PAGWindow {
273385
case "fullscreen-window":
274386
viewWindow.visibility = viewWindow.visibility !== Window.Maximized ? Window.Maximized : Window.AutomaticVisibility;
275387
break;
388+
case "export-frame-as-png":
389+
if (openFileDialog.currentAcceptHandler) {
390+
openFileDialog.accepted.disconnect(openFileDialog.currentAcceptHandler);
391+
}
392+
openFileDialog.fileMode = FileDialog.SaveFile;
393+
openFileDialog.title = qsTr("Select save path");
394+
openFileDialog.nameFilters = ["PNG files(*.png)"];
395+
openFileDialog.defaultSuffix = "png";
396+
openFileDialog.currentFolder = Utils.getFileDir(mainForm.pagView.filePath);
397+
openFileDialog.currentAcceptHandler = function () {
398+
let filePath = openFileDialog.selectedFile;
399+
let task = taskFactory.createTask(PAGTaskFactory.PAGTaskType_ExportPNG, filePath, {
400+
"exportFrame": mainForm.pagView.currentFrame
401+
});
402+
if (task) {
403+
taskConnections.target = task;
404+
progressWindow.title = qsTr("Exporting");
405+
progressWindow.task = task;
406+
progressWindow.visible = true;
407+
progressWindow.raise();
408+
task.start();
409+
}
410+
};
411+
openFileDialog.accepted.connect(openFileDialog.currentAcceptHandler);
412+
openFileDialog.open();
413+
break;
414+
case "export-as-png-sequence":
415+
if (openFolderDialog.currentAcceptHandler) {
416+
openFolderDialog.accepted.disconnect(openFolderDialog.currentAcceptHandler);
417+
}
418+
openFolderDialog.title = qsTr("Select save path");
419+
openFolderDialog.currentFolder = Utils.getFileDir(mainForm.pagView.filePath);
420+
openFolderDialog.currentAcceptHandler = function () {
421+
let filePath = openFolderDialog.folder;
422+
let task = taskFactory.createTask(PAGTaskFactory.PAGTaskType_ExportPNG, filePath, {});
423+
if (task) {
424+
taskConnections.target = task;
425+
progressWindow.title = qsTr("Exporting");
426+
progressWindow.progressBar.value = 0;
427+
progressWindow.task = task;
428+
progressWindow.visible = true;
429+
progressWindow.raise();
430+
task.start();
431+
}
432+
};
433+
openFolderDialog.accepted.connect(openFolderDialog.currentAcceptHandler);
434+
openFolderDialog.open();
435+
break;
436+
case "export-as-apng":
437+
if (openFileDialog.currentAcceptHandler) {
438+
openFileDialog.accepted.disconnect(openFileDialog.currentAcceptHandler);
439+
}
440+
openFileDialog.fileMode = FileDialog.SaveFile;
441+
openFileDialog.title = qsTr("Select save path");
442+
openFileDialog.nameFilters = ["APNG files(*.png)"];
443+
openFileDialog.defaultSuffix = "png";
444+
openFileDialog.currentFolder = Utils.getFileDir(mainForm.pagView.filePath);
445+
openFileDialog.currentAcceptHandler = function () {
446+
let filePath = openFileDialog.selectedFile;
447+
let task = taskFactory.createTask(PAGTaskFactory.PAGTaskType_ExportAPNG, filePath, {});
448+
if (task) {
449+
taskConnections.target = task;
450+
progressWindow.title = qsTr("Exporting");
451+
progressWindow.progressBar.value = 0;
452+
progressWindow.task = task;
453+
progressWindow.visible = true;
454+
progressWindow.raise();
455+
task.start();
456+
}
457+
};
458+
openFileDialog.accepted.connect(openFileDialog.currentAcceptHandler);
459+
openFileDialog.open();
460+
break;
276461
default:
277462
console.log(`Undefined command: [${command}]`);
278463
break;

viewer/qml/Menu.qml

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,32 @@ Item {
4848
root.command("open-preferences");
4949
}
5050
}
51+
PAGMenu {
52+
menuWidth: windowsMenuBar.menuWidth
53+
title: qsTr("Export")
54+
Action {
55+
text: qsTr("Export as PNG Sequence Frames")
56+
enabled: root.hasPAGFile
57+
onTriggered: {
58+
root.command('export-as-png-sequence');
59+
}
60+
}
61+
Action {
62+
text: qsTr("Export as APNG")
63+
enabled: root.hasPAGFile
64+
onTriggered: {
65+
root.command('export-as-apng');
66+
}
67+
}
68+
Action {
69+
text: qsTr("Export current frame as PNG")
70+
enabled: root.hasPAGFile
71+
shortcut: "Ctrl+P"
72+
onTriggered: {
73+
root.command('export-frame-as-png');
74+
}
75+
}
76+
}
5177
}
5278

5379
PAGMenu {
@@ -196,6 +222,31 @@ Item {
196222
root.command("open-pag-file");
197223
}
198224
}
225+
Platform.Menu {
226+
title: qsTr("Export")
227+
Platform.MenuItem {
228+
text: qsTr("Export as PNG Sequence Frames")
229+
enabled: root.hasPAGFile
230+
onTriggered: {
231+
root.command('export-as-png-sequence');
232+
}
233+
}
234+
Platform.MenuItem {
235+
text: qsTr("Export as APNG")
236+
enabled: root.hasPAGFile
237+
onTriggered: {
238+
root.command('export-as-apng');
239+
}
240+
}
241+
Platform.MenuItem {
242+
text: qsTr("Export current frame as PNG")
243+
enabled: root.hasPAGFile
244+
shortcut: "Meta+P"
245+
onTriggered: {
246+
root.command('export-frame-as-png');
247+
}
248+
}
249+
}
199250
}
200251
Platform.Menu {
201252
title: qsTr("Play")

viewer/qml/utils/Utils.qml

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,4 +23,13 @@ QtObject {
2323
let fileName = urlObject.pathname.split('/').pop();
2424
return fileName;
2525
}
26-
}
26+
27+
function replaceLastIndexOf(str, oldPara, newPara) {
28+
let lastIndex = str.lastIndexOf(oldPara);
29+
if (lastIndex !== -1) {
30+
return str;
31+
}
32+
33+
return str.substring(0, lastIndex) + newPara + str.substring(lastIndex + oldPara.length);
34+
}
35+
}

viewer/src/main.cpp

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
#include <QSGRendererInterface>
2323
#include "PAGViewer.h"
2424
#include "rendering/PAGView.h"
25+
#include "task/PAGTaskFactory.h"
2526

2627
int main(int argc, char* argv[]) {
2728
bool cpuMode = false;
@@ -60,6 +61,7 @@ int main(int argc, char* argv[]) {
6061
pag::PAGViewer app(argc, argv);
6162
QApplication::setWindowIcon(QIcon(":/images/window-icon.png"));
6263
qmlRegisterType<pag::PAGView>("PAG", 1, 0, "PAGView");
64+
qmlRegisterType<pag::PAGTaskFactory>("PAG", 1, 0, "PAGTaskFactory");
6365
app.openFile(filePath.data());
6466

6567
return QApplication::exec();
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
/////////////////////////////////////////////////////////////////////////////////////////////////
2+
//
3+
// Tencent is pleased to support the open source community by making libpag available.
4+
//
5+
// Copyright (C) 2021 THL A29 Limited, a Tencent company. All rights reserved.
6+
//
7+
// Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file
8+
// except in compliance with the License. You may obtain a copy of the License at
9+
//
10+
// http://www.apache.org/licenses/LICENSE-2.0
11+
//
12+
// unless required by applicable law or agreed to in writing, software distributed under the
13+
// license is distributed on an "as is" basis, without warranties or conditions of any kind,
14+
// either express or implied. see the license for the specific language governing permissions
15+
// and limitations under the license.
16+
//
17+
/////////////////////////////////////////////////////////////////////////////////////////////////
18+
19+
#pragma once
20+
21+
#include <QFileInfo>
22+
23+
namespace pag::Utils {
24+
25+
auto openFileInFinder(QFileInfo& fileInfo) -> void;
26+
27+
} // namespace pag::Utils

0 commit comments

Comments
 (0)