All Classes Namespaces Functions Variables Enumerations Properties Pages
test_filemanager.cpp
1 /*
2 
3 Pencil2D - Traditional Animation Software
4 Copyright (C) 2012-2020 Matthew Chiawen Chang
5 
6 This program is free software; you can redistribute it and/or
7 modify it under the terms of the GNU General Public License
8 as published by the Free Software Foundation; version 2 of the License.
9 
10 This program is distributed in the hope that it will be useful,
11 but WITHOUT ANY WARRANTY; without even the implied warranty of
12 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 GNU General Public License for more details.
14 
15 */
16 #include "catch.hpp"
17 
18 #include <QTemporaryDir>
19 #include <QTemporaryFile>
20 #include <QImage>
21 #include "qminiz.h"
22 #include "fileformat.h"
23 #include "filemanager.h"
24 #include "util.h"
25 #include "object.h"
26 #include "bitmapimage.h"
27 #include "layerbitmap.h"
28 
29 
30 TEST_CASE("FileManager Initial Test")
31 {
32  SECTION("Initial error code")
33  {
34  FileManager f;
35  REQUIRE(f.error() == Status::OK);
36  }
37 }
38 
39 TEST_CASE("FileManager invalid operations")
40 {
41  SECTION("Open a non-existing file")
42  {
43  FileManager fm;
44 
45  QString strDummyPath = "hahaha_blala.pcl";
46  Object* obj = fm.load(strDummyPath);
47 
48  REQUIRE(obj == nullptr);
49  REQUIRE(fm.error().code() == Status::FILE_NOT_FOUND);
50  }
51 
52  SECTION("Bad XML")
53  {
54  QString strBadXMLPath = QDir::tempPath() + "/bad.pcl";
55 
56  // make a fake xml.
57  QFile badXMLFile(strBadXMLPath);
58  badXMLFile.open(QIODevice::WriteOnly);
59 
60  QTextStream fout(&badXMLFile);
61  fout << "%% haha, this is not a xml file.";
62  badXMLFile.close();
63 
64  FileManager fm;
65  Object* pObj = fm.load(strBadXMLPath);
66 
67  REQUIRE(pObj == nullptr);
68  REQUIRE(fm.error().code() == Status::ERROR_INVALID_XML_FILE);
69  }
70 
71  SECTION("A valid xml, but doesn't follow pencil2d's rule")
72  {
73  QString strBadXMLPath = QDir::tempPath() + "/bad.pcl";
74 
75  QFile badXMLFile(strBadXMLPath);
76  badXMLFile.open(QIODevice::WriteOnly);
77 
78  QTextStream fout(&badXMLFile);
79  fout << "<!DOCTYPE BlahBlahRoot><document></document>";
80  badXMLFile.close();
81 
82  FileManager fm;
83  Object* pObj = fm.load(strBadXMLPath);
84 
85  REQUIRE(pObj == nullptr);
86  REQUIRE(fm.error().code() == Status::ERROR_INVALID_PENCIL_FILE);
87  }
88 }
89 
90 TEST_CASE("FileManager Loading XML Tests")
91 {
92  SECTION("Minimal working xml")
93  {
94  QTemporaryFile minimalDoc;
95  if (minimalDoc.open())
96  {
97  QFile minXML(minimalDoc.fileName());
98  minXML.open(QIODevice::WriteOnly);
99 
100  QTextStream fout(&minXML);
101  fout << "<!DOCTYPE PencilDocument><document>";
102  fout << " <object></object>";
103  fout << "</document>";
104  minXML.close();
105 
106  FileManager fm;
107  Object* o = fm.load(minimalDoc.fileName());
108 
109  REQUIRE(o != nullptr);
110  REQUIRE(fm.error().ok());
111  REQUIRE(o->getLayerCount() == 1); // have at least one cam layer
112 
113  delete o;
114  }
115  }
116 
117  SECTION("Xml with one bitmap layer")
118  {
119  QTemporaryFile tmpFile;
120  if (!tmpFile.open())
121  {
122  REQUIRE(false);
123  }
124  QFile theXML(tmpFile.fileName());
125  theXML.open(QIODevice::WriteOnly);
126 
127  QTextStream fout(&theXML);
128  fout << "<!DOCTYPE PencilDocument><document>";
129  fout << " <object>";
130  fout << " <layer name='MyLayer' id='5' visibility='1' type='1'></layer>";
131  fout << " </object>";
132  fout << "</document>";
133  theXML.close();
134 
135  FileManager fm;
136  Object* obj = fm.load(theXML.fileName());
137  REQUIRE(obj->getLayerCount() == 2); // one bitmap layer and one default cam layer
138  REQUIRE(obj->getLayer(0)->name() == "MyLayer");
139  REQUIRE(obj->getLayer(0)->id() == 5);
140  REQUIRE(obj->getLayer(0)->visible() == true);
141  REQUIRE(obj->getLayer(0)->type() == Layer::BITMAP);
142 
143  delete obj;
144  }
145 
146  SECTION("xml with one bitmap layer and one key")
147  {
148  QTemporaryFile tmpFile;
149  if (!tmpFile.open())
150  {
151  REQUIRE(false);
152  }
153  QFile theXML(tmpFile.fileName());
154  theXML.open(QIODevice::WriteOnly);
155 
156  QTextStream fout(&theXML);
157  fout << "<!DOCTYPE PencilDocument><document>";
158  fout << " <object>";
159  fout << " <layer name='GoodLayer' id='5' visibility='0' type='1' >";
160  fout << " <image frame='1' topLeftY='0' src='003.001.png' topLeftX='0' />";
161  fout << " </layer>";
162  fout << " </object>";
163  fout << "</document>";
164  theXML.close();
165 
166  FileManager fm;
167  Object* obj = fm.load(theXML.fileName());
168 
169  Layer* layer = obj->getLayer(0);
170  REQUIRE(layer->name() == "GoodLayer");
171  REQUIRE(layer->id() == 5);
172  REQUIRE(layer->visible() == false);
173  REQUIRE(layer->type() == Layer::BITMAP);
174 
175  REQUIRE(layer->getKeyFrameAt(1) != nullptr);
176  REQUIRE(layer->keyFrameCount() == 1);
177 
178  delete obj;
179  }
180 
181  SECTION("xml with 1 bitmap layer and 2 keys")
182  {
183  QTemporaryFile tmpFile;
184  if (!tmpFile.open())
185  {
186  REQUIRE(false);
187  }
188  QFile theXML(tmpFile.fileName());
189  theXML.open(QIODevice::WriteOnly);
190 
191  QTextStream fout(&theXML);
192  fout << "<!DOCTYPE PencilDocument><document>";
193  fout << " <object>";
194  fout << " <layer name='MyLayer' id='5' visibility='1' type='1' >";
195  fout << " <image frame='1' topLeftY='0' src='003.001.png' topLeftX='0' />";
196  fout << " <image frame='2' topLeftY='0' src='003.001.png' topLeftX='0' />";
197  fout << " </layer>";
198  fout << " </object>";
199  fout << "</document>";
200  theXML.close();
201 
202  FileManager fm;
203  Object* obj = fm.load(theXML.fileName());
204 
205  Layer* layer = obj->getLayer(0);
206  REQUIRE(layer->name() == "MyLayer");
207  REQUIRE(layer->id() == 5);
208  REQUIRE(layer->visible() == true);
209  REQUIRE(layer->type() == Layer::BITMAP);
210 
211  REQUIRE(layer->getKeyFrameAt(1) != nullptr);
212  delete obj;
213  }
214 }
215 
216 // Turn a Qt resource file into an actual file on disk
217 QString QtResourceToFile(QString rscPath, QString filename, QTemporaryDir& tempDir)
218 {
219  QFile fin(rscPath);
220  if (!fin.open(QFile::ReadOnly))
221  {
222  qWarning() << __FUNCTION__ << "Cannot open" << rscPath;
223  return "";
224  }
225  QByteArray content = fin.readAll();
226  fin.close();
227 
228  QString filePathOnDisk = tempDir.filePath(filename);
229  QFile fout(filePathOnDisk);
230  if (!fout.open(QFile::WriteOnly))
231  {
232  qWarning() << __FUNCTION__ << "Cannot write to" << filePathOnDisk;
233  }
234  fout.write(content);
235  fout.close();
236  return filePathOnDisk;
237 }
238 
239 TEST_CASE("FileManager Load PCLX")
240 {
241  SECTION("Empty PCLX")
242  {
243  QTemporaryDir tempDir;
244 
245  FileManager fm;
246  Object* o = fm.load(QtResourceToFile(":/empty.pclx", "empty.pclx", tempDir));
247  REQUIRE(o != nullptr);
248  if (o)
249  {
250  // file has 2 bitmap layers, 1 vector layers and 1 cam layers
251  REQUIRE(o->getLayerCount() == 4);
252  }
253  delete o;
254  }
255 
256  SECTION("Chinese Filename")
257  {
258  QTemporaryDir tempDir;
259 
260  FileManager fm;
261  Object* o = fm.load(QtResourceToFile(":/cjk-test.pclx", "許功蓋.pclx", tempDir));
262  REQUIRE(o != nullptr);
263  if (o)
264  {
265  // file has 2 bitmap layers, 1 vector layers and 1 cam layers
266  REQUIRE(o->getLayerCount() == 4);
267  }
268  delete o;
269  }
270 
271  SECTION("Japanese Filename")
272  {
273  QTemporaryDir tempDir;
274 
275  FileManager fm;
276  Object* o = fm.load(QtResourceToFile(":/cjk-test.pclx", "構わない.pclx", tempDir));
277  REQUIRE(o != nullptr);
278  if (o)
279  {
280  // file has 2 bitmap layers, 1 vector layers and 1 cam layers
281  REQUIRE(o->getLayerCount() == 4);
282  }
283  delete o;
284  }
285 
286  SECTION("Korean Filename")
287  {
288  QTemporaryDir tempDir;
289 
290  FileManager fm;
291  Object* o = fm.load(QtResourceToFile(":/cjk-test.pclx", "대박이야.pclx", tempDir));
292  REQUIRE(o != nullptr);
293  if (o)
294  {
295  // file has 2 bitmap layers, 1 vector layers and 1 cam layers
296  REQUIRE(o->getLayerCount() == 4);
297  }
298  delete o;
299  }
300 }
301 
302 TEST_CASE("FileManager File-saving")
303 {
304  // https://github.com/pencil2d/pencil/issues/939
305  SECTION("#939 Clear action not properly saved")
306  {
307  FileManager fm;
308 
309  // 1. create a animation with one red frame & save it
310  Object* o1 = new Object;
311  o1->init();
312  o1->addNewCameraLayer();
313  o1->addNewVectorLayer();
314  o1->addNewBitmapLayer();
315 
316  LayerBitmap* layer = dynamic_cast<LayerBitmap*>(o1->getLayer(2));
317  REQUIRE(layer->addNewKeyFrameAt(2));
318 
319  BitmapImage* b1 = layer->getBitmapImageAtFrame(2);
320  b1->drawRect(QRectF(0, 0, 10, 10), QPen(QColor(255, 0, 0)), QBrush(Qt::red), QPainter::CompositionMode_SourceOver, false);
321 
322  QTemporaryDir testDir("PENCIL_TEST_XXXXXXXX");
323  QString animationPath = testDir.path() + "/abc.pclx";
324  fm.save(o1, animationPath);
325  delete o1;
326 
327  // 2. load the animation, and then clear the red frame, save it.
328  Object* o2 = fm.load(animationPath);
329  layer = dynamic_cast<LayerBitmap*>(o2->getLayer(2));
330 
331  BitmapImage* b2 = layer->getBitmapImageAtFrame(2);
332  b2->clear();
333 
334  fm.save(o2, animationPath);
335  delete o2;
336 
337  // 3. load the animation again, check whether it's an empty frame
338  Object* o3 = fm.load(animationPath);
339  layer = dynamic_cast<LayerBitmap*>(o3->getLayer(2));
340 
341  BitmapImage* b3 = layer->getBitmapImageAtFrame(2);
342  REQUIRE(b3->bounds().isEmpty());
343 
344  delete o3;
345  }
346 
347  //https://github.com/pencil2d/pencil/issues/966
348  SECTION("#966 Moving more than 200 frames corrupts frames upon save")
349  {
350  FileManager fm;
351 
352  // 1. Create a animation with 150 frames & save it
353  Object* o1 = new Object;
354  o1->init();
355  o1->addNewCameraLayer();
356  o1->addNewVectorLayer();
357  o1->addNewBitmapLayer();
358 
359  LayerBitmap* layer = dynamic_cast<LayerBitmap*>(o1->getLayer(2));
360  for (int i = 100; i < 150; ++i)
361  {
362  layer->addNewKeyFrameAt(i);
363  auto bitmap = layer->getBitmapImageAtFrame(i);
364  bitmap->drawRect(QRectF(0, 0, 10, 10), QPen(QColor(255, 0, 0)), QBrush(Qt::red), QPainter::CompositionMode_SourceOver, false);
365  }
366 
367  QTemporaryDir testDir("PENCIL_TEST_XXXXXXXX");
368  QString animationPath = testDir.path() + "/abc.pclx";
369  fm.save(o1, animationPath);
370  delete o1;
371 
372  // 2. Load the animation back and then make some frames unloaded by active frame pool
373  Object* o2 = fm.load(animationPath);
374  o2->setActiveFramePoolSize(20);
375 
376  layer = dynamic_cast<LayerBitmap*>(o2->getLayer(2));
377  for (int i = 1; i < 150; ++i)
378  o2->updateActiveFrames(i);
379 
380  // 3. Move those unloaded frames around
381  for (int i = 100; i < 150; ++i)
382  layer->setFrameSelected(i, true);
383 
384  layer->moveSelectedFrames(-55);
385  fm.save(o2, animationPath);
386  delete o2;
387 
388  // 4. Check no lost frames
389  Object* o3 = fm.load(animationPath);
390  layer = dynamic_cast<LayerBitmap*>(o3->getLayer(2));
391  for (int i = 2; i < 150; ++i)
392  {
393  auto bitmap = layer->getBitmapImageAtFrame(i);
394  if (bitmap)
395  {
396  REQUIRE(bitmap->image()->width() > 1);
397  REQUIRE(bitmap->image()->height() > 1);
398  }
399  }
400  delete o3;
401  }
402 }
403 
404 TEST_CASE("Empty Sound Frames")
405 {
406  SECTION("Invalid src value")
407  {
408 
409  QTemporaryFile soundFrameDoc;
410  if (soundFrameDoc.open())
411  {
412  QFile newXML(soundFrameDoc.fileName());
413  newXML.open(QIODevice::WriteOnly);
414 
415  QTextStream fout(&newXML);
416  fout << "<!DOCTYPE PencilDocument><document>";
417  fout << " <object>";
418  fout << " <layer type='4' id='5' name='GoodLayer' visibility='1'>";
419  fout << " <sound frame='1' name='' src=''/>";
420  fout << " </layer>";
421  fout << " </object>";
422  fout << "</document>";
423  newXML.close();
424 
425 
426  FileManager fm;
427  Object* newObj = fm.load(soundFrameDoc.fileName());
428 
429  REQUIRE(newObj != nullptr);
430  REQUIRE(fm.error().ok());
431  REQUIRE(newObj->getLayerCount() == 2);
432  REQUIRE(newObj->getLayer(0)->type() == 4);
433  REQUIRE(newObj->getLayer(0)->id() == 5);
434  REQUIRE(newObj->getLayer(0)->name() == "GoodLayer");
435  REQUIRE(newObj->getLayer(0)->visible() == true);
436  REQUIRE(newObj->getLayer(0)->getKeyFrameAt(1) == nullptr);
437 
438  delete newObj;
439  }
440  }
441 }
QString tempPath()
QString filePath(const QString &fileName) const const
Definition: layer.h:38
virtual bool open(QIODevice::OpenMode mode) override
CompositionMode_SourceOver
bool isEmpty() const const
virtual QString fileName() const const override
Definition: object.h:41