1//============================================================================
2//
3// SSSS tt lll lll
4// SS SS tt ll ll
5// SS tttttt eeee ll ll aaaa
6// SSSS tt ee ee ll ll aa
7// SS tt eeeeee ll ll aaaaa -- "An Atari 2600 VCS Emulator"
8// SS SS tt ee ll ll aa aa
9// SSSS ttt eeeee llll llll aaaaa
10//
11// Copyright (c) 1995-2019 by Bradford W. Mott, Stephen Anthony
12// and the Stella Team
13//
14// See the file "License.txt" for information on usage and redistribution of
15// this file, and for a DISCLAIMER OF ALL WARRANTIES.
16//============================================================================
17
18#if defined(PNG_SUPPORT)
19
20#include <cmath>
21
22#include "bspf.hxx"
23#include "OSystem.hxx"
24#include "Console.hxx"
25#include "FrameBuffer.hxx"
26#include "FBSurface.hxx"
27#include "Props.hxx"
28#include "Settings.hxx"
29#include "TIASurface.hxx"
30#include "Version.hxx"
31#include "PNGLibrary.hxx"
32
33// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
34PNGLibrary::PNGLibrary(OSystem& osystem)
35 : myOSystem(osystem),
36 mySnapInterval(0),
37 mySnapCounter(0)
38{
39}
40
41// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
42void PNGLibrary::loadImage(const string& filename, FBSurface& surface)
43{
44 png_structp png_ptr = nullptr;
45 png_infop info_ptr = nullptr;
46 png_uint_32 iwidth, iheight;
47 int bit_depth, color_type, interlace_type;
48
49 auto loadImageERROR = [&](const char* s) {
50 if(png_ptr)
51 png_destroy_read_struct(&png_ptr, info_ptr ? &info_ptr : nullptr, nullptr);
52 if(s)
53 throw runtime_error(s);
54 };
55
56 ifstream in(filename, std::ios_base::binary);
57 if(!in.is_open())
58 loadImageERROR("No snapshot found");
59
60 // Create the PNG loading context structure
61 png_ptr = png_create_read_struct(PNG_LIBPNG_VER_STRING, nullptr,
62 png_user_error, png_user_warn);
63 if(png_ptr == nullptr)
64 loadImageERROR("Couldn't allocate memory for PNG file");
65
66 // Allocate/initialize the memory for image information. REQUIRED.
67 info_ptr = png_create_info_struct(png_ptr);
68 if(info_ptr == nullptr)
69 loadImageERROR("Couldn't create image information for PNG file");
70
71 // Set up the input control
72 png_set_read_fn(png_ptr, &in, png_read_data);
73
74 // Read PNG header info
75 png_read_info(png_ptr, info_ptr);
76 png_get_IHDR(png_ptr, info_ptr, &iwidth, &iheight, &bit_depth,
77 &color_type, &interlace_type, nullptr, nullptr);
78
79 // Tell libpng to strip 16 bit/color files down to 8 bits/color
80 png_set_strip_16(png_ptr);
81
82 // Extract multiple pixels with bit depths of 1, 2, and 4 from a single
83 // byte into separate bytes (useful for paletted and grayscale images).
84 png_set_packing(png_ptr);
85
86 // Only normal RBG(A) images are supported (without the alpha channel)
87 if(color_type == PNG_COLOR_TYPE_RGBA)
88 {
89 png_set_strip_alpha(png_ptr);
90 }
91 else if(color_type == PNG_COLOR_TYPE_GRAY || color_type == PNG_COLOR_TYPE_GRAY_ALPHA)
92 {
93 loadImageERROR("Greyscale PNG images not supported");
94 }
95 else if(color_type == PNG_COLOR_TYPE_PALETTE)
96 {
97 png_set_palette_to_rgb(png_ptr);
98 }
99 else if(color_type != PNG_COLOR_TYPE_RGB)
100 {
101 loadImageERROR("Unknown format in PNG image");
102 }
103
104 // Create/initialize storage area for the current image
105 if(!allocateStorage(iwidth, iheight))
106 loadImageERROR("Not enough memory to read PNG file");
107
108 // The PNG read function expects an array of rows, not a single 1-D array
109 for(uInt32 irow = 0, offset = 0; irow < ReadInfo.height; ++irow, offset += ReadInfo.pitch)
110 ReadInfo.row_pointers[irow] = png_bytep(ReadInfo.buffer.get() + offset);
111
112 // Read the entire image in one go
113 png_read_image(png_ptr, ReadInfo.row_pointers.get());
114
115 // We're finished reading
116 png_read_end(png_ptr, info_ptr);
117
118 // Load image into the surface, setting the correct dimensions
119 loadImagetoSurface(surface);
120
121 // Cleanup
122 if(png_ptr)
123 png_destroy_read_struct(&png_ptr, info_ptr ? &info_ptr : nullptr, nullptr);
124}
125
126// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
127void PNGLibrary::saveImage(const string& filename, const VariantList& comments)
128{
129 ofstream out(filename, std::ios_base::binary);
130 if(!out.is_open())
131 throw runtime_error("ERROR: Couldn't create snapshot file");
132
133 const FrameBuffer& fb = myOSystem.frameBuffer();
134 const Common::Rect& rect = fb.imageRect();
135 png_uint_32 width = rect.w(), height = rect.h();
136
137 // Get framebuffer pixel data (we get ABGR format)
138 unique_ptr<png_byte[]> buffer = make_unique<png_byte[]>(width * height * 4);
139 fb.readPixels(buffer.get(), width*4, rect);
140
141 // Set up pointers into "buffer" byte array
142 unique_ptr<png_bytep[]> rows = make_unique<png_bytep[]>(height);
143 for(png_uint_32 k = 0; k < height; ++k)
144 rows[k] = png_bytep(buffer.get() + k*width*4);
145
146 // And save the image
147 saveImageToDisk(out, rows, width, height, comments);
148}
149
150// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
151void PNGLibrary::saveImage(const string& filename, const FBSurface& surface,
152 const Common::Rect& rect, const VariantList& comments)
153{
154 ofstream out(filename, std::ios_base::binary);
155 if(!out.is_open())
156 throw runtime_error("ERROR: Couldn't create snapshot file");
157
158 // Do we want the entire surface or just a section?
159 png_uint_32 width = rect.w(), height = rect.h();
160 if(rect.empty())
161 {
162 width = surface.width();
163 height = surface.height();
164 }
165
166 // Get the surface pixel data (we get ABGR format)
167 unique_ptr<png_byte[]> buffer = make_unique<png_byte[]>(width * height * 4);
168 surface.readPixels(buffer.get(), width, rect);
169
170 // Set up pointers into "buffer" byte array
171 unique_ptr<png_bytep[]> rows = make_unique<png_bytep[]>(height);
172 for(png_uint_32 k = 0; k < height; ++k)
173 rows[k] = png_bytep(buffer.get() + k*width*4);
174
175 // And save the image
176 saveImageToDisk(out, rows, width, height, comments);
177}
178
179// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
180void PNGLibrary::saveImageToDisk(ofstream& out, const unique_ptr<png_bytep[]>& rows,
181 png_uint_32 width, png_uint_32 height, const VariantList& comments)
182{
183 png_structp png_ptr = nullptr;
184 png_infop info_ptr = nullptr;
185
186 auto saveImageERROR = [&](const char* s) {
187 if(png_ptr)
188 png_destroy_write_struct(&png_ptr, &info_ptr);
189 if(s)
190 throw runtime_error(s);
191 };
192
193 // Create the PNG saving context structure
194 png_ptr = png_create_write_struct(PNG_LIBPNG_VER_STRING, nullptr,
195 png_user_error, png_user_warn);
196 if(png_ptr == nullptr)
197 saveImageERROR("Couldn't allocate memory for PNG file");
198
199 // Allocate/initialize the memory for image information. REQUIRED.
200 info_ptr = png_create_info_struct(png_ptr);
201 if(info_ptr == nullptr)
202 saveImageERROR("Couldn't create image information for PNG file");
203
204 // Set up the output control
205 png_set_write_fn(png_ptr, &out, png_write_data, png_io_flush);
206
207 // Write PNG header info
208 png_set_IHDR(png_ptr, info_ptr, width, height, 8,
209 PNG_COLOR_TYPE_RGB, PNG_INTERLACE_NONE, PNG_COMPRESSION_TYPE_DEFAULT,
210 PNG_FILTER_TYPE_DEFAULT);
211
212 // Write comments
213 writeComments(png_ptr, info_ptr, comments);
214
215 // Write the file header information. REQUIRED
216 png_write_info(png_ptr, info_ptr);
217
218 // Pack pixels into bytes
219 png_set_packing(png_ptr);
220
221 // Swap location of alpha bytes from ARGB to RGBA
222 png_set_swap_alpha(png_ptr);
223
224 // Pack ARGB into RGB
225 png_set_filler(png_ptr, 0, PNG_FILLER_AFTER);
226
227 // Flip BGR pixels to RGB
228 png_set_bgr(png_ptr);
229
230 // Write the entire image in one go
231 png_write_image(png_ptr, rows.get());
232
233 // We're finished writing
234 png_write_end(png_ptr, info_ptr);
235
236 // Cleanup
237 if(png_ptr)
238 png_destroy_write_struct(&png_ptr, &info_ptr);
239}
240
241// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
242void PNGLibrary::updateTime(uInt64 time)
243{
244 if(++mySnapCounter % mySnapInterval == 0)
245 takeSnapshot(uInt32(time) >> 10); // not quite milliseconds, but close enough
246}
247
248// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
249void PNGLibrary::toggleContinuousSnapshots(bool perFrame)
250{
251 if(mySnapInterval == 0)
252 {
253 ostringstream buf;
254 uInt32 interval = myOSystem.settings().getInt("ssinterval");
255 if(perFrame)
256 {
257 buf << "Enabling snapshots every frame";
258 interval = 1;
259 }
260 else
261 {
262 buf << "Enabling snapshots in " << interval << " second intervals";
263 interval *= uInt32(myOSystem.frameRate());
264 }
265 myOSystem.frameBuffer().showMessage(buf.str());
266 setContinuousSnapInterval(interval);
267 }
268 else
269 {
270 ostringstream buf;
271 buf << "Disabling snapshots, generated "
272 << (mySnapCounter / mySnapInterval)
273 << " files";
274 myOSystem.frameBuffer().showMessage(buf.str());
275 setContinuousSnapInterval(0);
276 }
277}
278
279// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
280void PNGLibrary::setContinuousSnapInterval(uInt32 interval)
281{
282 mySnapInterval = interval;
283 mySnapCounter = 0;
284}
285
286// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
287void PNGLibrary::takeSnapshot(uInt32 number)
288{
289 if(!myOSystem.hasConsole())
290 return;
291
292 // Figure out the correct snapshot name
293 string filename;
294 bool showmessage = number == 0;
295 string sspath = myOSystem.snapshotSaveDir() +
296 (myOSystem.settings().getString("snapname") != "int" ?
297 myOSystem.romFile().getNameWithExt("")
298 : myOSystem.console().properties().get(PropType::Cart_Name));
299
300 // Check whether we want multiple snapshots created
301 if(number > 0)
302 {
303 ostringstream buf;
304 buf << sspath << "_" << std::hex << std::setw(8) << std::setfill('0')
305 << number << ".png";
306 filename = buf.str();
307 }
308 else if(!myOSystem.settings().getBool("sssingle"))
309 {
310 // Determine if the file already exists, checking each successive filename
311 // until one doesn't exist
312 filename = sspath + ".png";
313 FilesystemNode node(filename);
314 if(node.exists())
315 {
316 ostringstream buf;
317 for(uInt32 i = 1; ;++i)
318 {
319 buf.str("");
320 buf << sspath << "_" << i << ".png";
321 FilesystemNode next(buf.str());
322 if(!next.exists())
323 break;
324 }
325 filename = buf.str();
326 }
327 }
328 else
329 filename = sspath + ".png";
330
331 // Some text fields to add to the PNG snapshot
332 VariantList comments;
333 ostringstream version;
334 version << "Stella " << STELLA_VERSION << " (Build " << STELLA_BUILD << ") ["
335 << BSPF::ARCH << "]";
336 VarList::push_back(comments, "Software", version.str());
337 const string& name = (myOSystem.settings().getString("snapname") == "int")
338 ? myOSystem.console().properties().get(PropType::Cart_Name)
339 : myOSystem.romFile().getName();
340 VarList::push_back(comments, "ROM Name", name);
341 VarList::push_back(comments, "ROM MD5", myOSystem.console().properties().get(PropType::Cart_MD5));
342 VarList::push_back(comments, "TV Effects", myOSystem.frameBuffer().tiaSurface().effectsInfo());
343
344 // Now create a PNG snapshot
345 if(myOSystem.settings().getBool("ss1x"))
346 {
347 string message = "Snapshot saved";
348 try
349 {
350 Common::Rect rect;
351 const FBSurface& surface = myOSystem.frameBuffer().tiaSurface().baseSurface(rect);
352 myOSystem.png().saveImage(filename, surface, rect, comments);
353 }
354 catch(const runtime_error& e)
355 {
356 message = e.what();
357 }
358 if(showmessage)
359 myOSystem.frameBuffer().showMessage(message);
360 }
361 else
362 {
363 // Make sure we have a 'clean' image, with no onscreen messages
364 myOSystem.frameBuffer().enableMessages(false);
365 myOSystem.frameBuffer().tiaSurface().renderForSnapshot();
366
367 string message = "Snapshot saved";
368 try
369 {
370 myOSystem.png().saveImage(filename, comments);
371 }
372 catch(const runtime_error& e)
373 {
374 message = e.what();
375 }
376
377 // Re-enable old messages
378 myOSystem.frameBuffer().enableMessages(true);
379 if(showmessage)
380 myOSystem.frameBuffer().showMessage(message);
381 }
382}
383
384// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
385bool PNGLibrary::allocateStorage(png_uint_32 w, png_uint_32 h)
386{
387 // Create space for the entire image (3 bytes per pixel in RGB format)
388 uInt32 req_buffer_size = w * h * 3;
389 if(req_buffer_size > ReadInfo.buffer_size)
390 {
391 ReadInfo.buffer = make_unique<png_byte[]>(req_buffer_size);
392 if(ReadInfo.buffer == nullptr)
393 return false;
394
395 ReadInfo.buffer_size = req_buffer_size;
396 }
397 uInt32 req_row_size = h;
398 if(req_row_size > ReadInfo.row_size)
399 {
400 ReadInfo.row_pointers = make_unique<png_bytep[]>(req_row_size);
401 if(ReadInfo.row_pointers == nullptr)
402 return false;
403
404 ReadInfo.row_size = req_row_size;
405 }
406
407 ReadInfo.width = w;
408 ReadInfo.height = h;
409 ReadInfo.pitch = w * 3;
410
411 return true;
412}
413
414// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
415void PNGLibrary::loadImagetoSurface(FBSurface& surface)
416{
417 // First determine if we need to resize the surface
418 uInt32 iw = ReadInfo.width, ih = ReadInfo.height;
419 if(iw > surface.width() || ih > surface.height())
420 surface.resize(iw, ih);
421
422 // The source dimensions are set here; the destination dimensions are
423 // set by whoever owns the surface
424 surface.setSrcPos(0, 0);
425 surface.setSrcSize(iw, ih);
426
427 // Convert RGB triples into pixels and store in the surface
428 uInt32 *s_buf, s_pitch;
429 surface.basePtr(s_buf, s_pitch);
430 uInt8* i_buf = ReadInfo.buffer.get();
431 uInt32 i_pitch = ReadInfo.pitch;
432
433 const FrameBuffer& fb = myOSystem.frameBuffer();
434 for(uInt32 irow = 0; irow < ih; ++irow, i_buf += i_pitch, s_buf += s_pitch)
435 {
436 uInt8* i_ptr = i_buf;
437 uInt32* s_ptr = s_buf;
438 for(uInt32 icol = 0; icol < ReadInfo.width; ++icol, i_ptr += 3)
439 *s_ptr++ = fb.mapRGB(*i_ptr, *(i_ptr+1), *(i_ptr+2));
440 }
441}
442
443// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
444void PNGLibrary::writeComments(png_structp png_ptr, png_infop info_ptr,
445 const VariantList& comments)
446{
447 uInt32 numComments = uInt32(comments.size());
448 if(numComments == 0)
449 return;
450
451 unique_ptr<png_text[]> text_ptr = make_unique<png_text[]>(numComments);
452 for(uInt32 i = 0; i < numComments; ++i)
453 {
454 text_ptr[i].key = const_cast<char*>(comments[i].first.c_str());
455 text_ptr[i].text = const_cast<char*>(comments[i].second.toCString());
456 text_ptr[i].compression = PNG_TEXT_COMPRESSION_NONE;
457 text_ptr[i].text_length = 0;
458 }
459 png_set_text(png_ptr, info_ptr, text_ptr.get(), numComments);
460}
461
462// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
463void PNGLibrary::png_read_data(png_structp ctx, png_bytep area, png_size_t size)
464{
465 (static_cast<ifstream*>(png_get_io_ptr(ctx)))->read(
466 reinterpret_cast<char *>(area), size);
467}
468
469// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
470void PNGLibrary::png_write_data(png_structp ctx, png_bytep area, png_size_t size)
471{
472 (static_cast<ofstream*>(png_get_io_ptr(ctx)))->write(
473 reinterpret_cast<const char *>(area), size);
474}
475
476// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
477void PNGLibrary::png_io_flush(png_structp ctx)
478{
479 (static_cast<ofstream*>(png_get_io_ptr(ctx)))->flush();
480}
481
482// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
483void PNGLibrary::png_user_warn(png_structp ctx, png_const_charp str)
484{
485 throw runtime_error(string("PNGLibrary warning: ") + str);
486}
487
488// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
489void PNGLibrary::png_user_error(png_structp ctx, png_const_charp str)
490{
491 throw runtime_error(string("PNGLibrary error: ") + str);
492}
493
494// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
495PNGLibrary::ReadInfoType PNGLibrary::ReadInfo = {
496 nullptr, nullptr, 0, 0, 0, 0, 0
497};
498
499#endif // PNG_SUPPORT
500