1// Aseprite
2// Copyright (C) 2018-2022 Igara Studio S.A.
3// Copyright (C) 2001-2018 David Capello
4//
5// This program is distributed under the terms of
6// the End-User License Agreement for Aseprite.
7
8#ifdef HAVE_CONFIG_H
9#include "config.h"
10#endif
11
12#include "app/app.h"
13#include "app/doc.h"
14#include "app/file/file.h"
15#include "app/file/file_format.h"
16#include "app/file/format_options.h"
17#include "app/file/png_format.h"
18#include "app/file/png_options.h"
19#include "base/file_handle.h"
20#include "doc/doc.h"
21#include "gfx/color_space.h"
22
23#include <stdio.h>
24#include <stdlib.h>
25
26#include "png.h"
27
28#define PNG_TRACE(...) // TRACE
29
30namespace app {
31
32using namespace base;
33
34class PngFormat : public FileFormat {
35 const char* onGetName() const override {
36 return "png";
37 }
38
39 void onGetExtensions(base::paths& exts) const override {
40 exts.push_back("png");
41 }
42
43 dio::FileFormat onGetDioFormat() const override {
44 return dio::FileFormat::PNG_IMAGE;
45 }
46
47 int onGetFlags() const override {
48 return
49 FILE_SUPPORT_LOAD |
50 FILE_SUPPORT_SAVE |
51 FILE_SUPPORT_RGB |
52 FILE_SUPPORT_RGBA |
53 FILE_SUPPORT_GRAY |
54 FILE_SUPPORT_GRAYA |
55 FILE_SUPPORT_INDEXED |
56 FILE_SUPPORT_SEQUENCES |
57 FILE_SUPPORT_PALETTE_WITH_ALPHA |
58 FILE_ENCODE_ABSTRACT_IMAGE;
59 }
60
61 bool onLoad(FileOp* fop) override;
62 gfx::ColorSpaceRef loadColorSpace(png_structp png, png_infop info);
63#ifdef ENABLE_SAVE
64 bool onSave(FileOp* fop) override;
65 void saveColorSpace(png_structp png, png_infop info, const gfx::ColorSpace* colorSpace);
66#endif
67};
68
69FileFormat* CreatePngFormat()
70{
71 return new PngFormat;
72}
73
74static void report_png_error(png_structp png, png_const_charp error)
75{
76 ((FileOp*)png_get_error_ptr(png))->setError("libpng: %s\n", error);
77}
78
79// TODO this should be information in FileOp parameter of onSave()
80static bool fix_one_alpha_pixel = false;
81
82PngEncoderOneAlphaPixel::PngEncoderOneAlphaPixel(bool state)
83{
84 fix_one_alpha_pixel = state;
85}
86
87PngEncoderOneAlphaPixel::~PngEncoderOneAlphaPixel()
88{
89 fix_one_alpha_pixel = false;
90}
91
92namespace {
93
94class DestroyReadPng {
95 png_structp png;
96 png_infop info;
97public:
98 DestroyReadPng(png_structp png, png_infop info) : png(png), info(info) { }
99 ~DestroyReadPng() {
100 png_destroy_read_struct(&png, info ? &info: nullptr, nullptr);
101 }
102};
103
104class DestroyWritePng {
105 png_structp png;
106 png_infop info;
107public:
108 DestroyWritePng(png_structp png, png_infop info) : png(png), info(info) { }
109 ~DestroyWritePng() {
110 png_destroy_write_struct(&png, info ? &info: nullptr);
111 }
112};
113
114// As in png_fixed_point_to_float() in skia/src/codec/SkPngCodec.cpp
115float png_fixtof(png_fixed_point x)
116{
117 // We multiply by the same factor that libpng used to convert
118 // fixed point -> double. Since we want floats, we choose to
119 // do the conversion ourselves rather than convert
120 // fixed point -> double -> float.
121 return ((float)x) * 0.00001f;
122}
123
124png_fixed_point png_ftofix(float x)
125{
126 return x * 100000.0f;
127}
128
129int png_user_chunk(png_structp png, png_unknown_chunkp unknown)
130{
131 auto data = (std::shared_ptr<PngOptions>*)png_get_user_chunk_ptr(png);
132 std::shared_ptr<PngOptions>& opts = *data;
133
134 PNG_TRACE("PNG: Read unknown chunk '%c%c%c%c'\n",
135 unknown->name[0],
136 unknown->name[1],
137 unknown->name[2],
138 unknown->name[3]);
139
140 PngOptions::Chunk chunk;
141 chunk.location = unknown->location;
142 for (int i=0; i<4; ++i)
143 chunk.name.push_back(unknown->name[i]);
144 if (unknown->size > 0) {
145 chunk.data.resize(unknown->size);
146 std::copy(unknown->data,
147 unknown->data+unknown->size,
148 chunk.data.begin());
149 }
150 opts->addChunk(std::move(chunk));
151
152 return 1;
153}
154
155} // anonymous namespace
156
157bool PngFormat::onLoad(FileOp* fop)
158{
159 png_uint_32 width, height, y;
160 unsigned int sig_read = 0;
161 int bit_depth, color_type, interlace_type;
162 int num_palette;
163 png_colorp palette;
164 png_bytepp rows_pointer;
165 PixelFormat pixelFormat;
166
167 FileHandle handle(open_file_with_exception(fop->filename(), "rb"));
168 FILE* fp = handle.get();
169
170 /* Create and initialize the png_struct with the desired error handler
171 * functions. If you want to use the default stderr and longjump method,
172 * you can supply NULL for the last three parameters. We also supply the
173 * the compiler header file version, so that we know if the application
174 * was compiled with a compatible version of the library
175 */
176 png_structp png =
177 png_create_read_struct(PNG_LIBPNG_VER_STRING, (png_voidp)fop,
178 report_png_error, report_png_error);
179 if (png == nullptr) {
180 fop->setError("png_create_read_struct\n");
181 return false;
182 }
183
184 // Do don't check if the sRGB color profile is valid, it gives
185 // problems with sRGB IEC61966-2.1 color profile from Photoshop.
186 // See this thread: https://community.aseprite.org/t/2656
187 png_set_option(png, PNG_SKIP_sRGB_CHECK_PROFILE, PNG_OPTION_ON);
188
189 // Set a function to read user data chunks
190 auto opts = std::make_shared<PngOptions>();
191 png_set_read_user_chunk_fn(png, &opts, png_user_chunk);
192
193 /* Allocate/initialize the memory for image information. */
194 png_infop info = png_create_info_struct(png);
195 DestroyReadPng destroyer(png, info);
196 if (info == nullptr) {
197 fop->setError("png_create_info_struct\n");
198 return false;
199 }
200
201 /* Set error handling if you are using the setjmp/longjmp method (this is
202 * the normal method of doing things with libpng).
203 */
204 if (setjmp(png_jmpbuf(png))) {
205 fop->setError("Error reading PNG file\n");
206 return false;
207 }
208
209 /* Set up the input control if you are using standard C streams */
210 png_init_io(png, fp);
211
212 /* If we have already read some of the signature */
213 png_set_sig_bytes(png, sig_read);
214
215 /* The call to png_read_info() gives us all of the information from the
216 * PNG file before the first IDAT (image data chunk).
217 */
218 png_read_info(png, info);
219
220 png_get_IHDR(png, info, &width, &height, &bit_depth, &color_type,
221 &interlace_type, NULL, NULL);
222
223
224 /* Set up the data transformations you want. Note that these are all
225 * optional. Only call them if you want/need them. Many of the
226 * transformations only work on specific types of images, and many
227 * are mutually exclusive.
228 */
229
230 /* tell libpng to strip 16 bit/color files down to 8 bits/color */
231 png_set_strip_16(png);
232
233 /* Extract multiple pixels with bit depths of 1, 2, and 4 from a single
234 * byte into separate bytes (useful for paletted and grayscale images).
235 */
236 png_set_packing(png);
237
238 /* Expand grayscale images to the full 8 bits from 1, 2, or 4 bits/pixel */
239 if (color_type == PNG_COLOR_TYPE_GRAY && bit_depth < 8)
240 png_set_expand_gray_1_2_4_to_8(png);
241
242 /* Turn on interlace handling. REQUIRED if you are not using
243 * png_read_image(). To see how to handle interlacing passes,
244 * see the png_read_row() method below:
245 */
246 int number_passes = png_set_interlace_handling(png);
247
248 /* Optional call to gamma correct and add the background to the palette
249 * and update info structure.
250 */
251 png_read_update_info(png, info);
252
253 /* create the output image */
254 switch (png_get_color_type(png, info)) {
255
256 case PNG_COLOR_TYPE_RGB_ALPHA:
257 fop->sequenceSetHasAlpha(true);
258 [[fallthrough]];
259 case PNG_COLOR_TYPE_RGB:
260 pixelFormat = IMAGE_RGB;
261 break;
262
263 case PNG_COLOR_TYPE_GRAY_ALPHA:
264 fop->sequenceSetHasAlpha(true);
265 [[fallthrough]];
266 case PNG_COLOR_TYPE_GRAY:
267 pixelFormat = IMAGE_GRAYSCALE;
268 break;
269
270 case PNG_COLOR_TYPE_PALETTE:
271 pixelFormat = IMAGE_INDEXED;
272 break;
273
274 default:
275 fop->setError("Color type not supported\n)");
276 return false;
277 }
278
279 int imageWidth = png_get_image_width(png, info);
280 int imageHeight = png_get_image_height(png, info);
281 ImageRef image = fop->sequenceImage(pixelFormat, imageWidth, imageHeight);
282 if (!image) {
283 fop->setError("file_sequence_image %dx%d\n", imageWidth, imageHeight);
284 return false;
285 }
286
287 // Transparent color
288 png_color_16p png_trans_color = NULL;
289
290 // Read the palette
291 if (png_get_color_type(png, info) == PNG_COLOR_TYPE_PALETTE &&
292 png_get_PLTE(png, info, &palette, &num_palette)) {
293 fop->sequenceSetNColors(num_palette);
294
295 for (int c=0; c<num_palette; ++c) {
296 fop->sequenceSetColor(c,
297 palette[c].red,
298 palette[c].green,
299 palette[c].blue);
300 }
301
302 // Read alpha values for palette entries
303 png_bytep trans = NULL; // Transparent palette entries
304 int num_trans = 0;
305 int mask_entry = -1;
306
307 png_get_tRNS(png, info, &trans, &num_trans, nullptr);
308
309 for (int i = 0; i < num_trans; ++i) {
310 fop->sequenceSetAlpha(i, trans[i]);
311
312 if (trans[i] < 255) {
313 fop->sequenceSetHasAlpha(true); // Is a transparent sprite
314 if (trans[i] == 0) {
315 if (mask_entry < 0)
316 mask_entry = i;
317 }
318 }
319 }
320
321 if (mask_entry >= 0)
322 fop->document()->sprite()->setTransparentColor(mask_entry);
323 }
324 else {
325 png_get_tRNS(png, info, nullptr, nullptr, &png_trans_color);
326 }
327
328 // Allocate the memory to hold the image using the fields of info.
329 rows_pointer = (png_bytepp)png_malloc(png, sizeof(png_bytep) * height);
330 for (y = 0; y < height; y++)
331 rows_pointer[y] = (png_bytep)png_malloc(png, png_get_rowbytes(png, info));
332
333 for (int pass=0; pass<number_passes; ++pass) {
334 for (y = 0; y < height; y++) {
335 png_read_rows(png, rows_pointer+y, nullptr, 1);
336
337 fop->setProgress(
338 (double)((double)pass + (double)(y+1) / (double)(height))
339 / (double)number_passes);
340
341 if (fop->isStop())
342 break;
343 }
344 }
345
346 // Convert rows_pointer into the doc::Image
347 for (y = 0; y < height; y++) {
348 // RGB_ALPHA
349 if (png_get_color_type(png, info) == PNG_COLOR_TYPE_RGB_ALPHA) {
350 uint8_t* src_address = rows_pointer[y];
351 uint32_t* dst_address = (uint32_t*)image->getPixelAddress(0, y);
352 unsigned int x, r, g, b, a;
353
354 for (x=0; x<width; x++) {
355 r = *(src_address++);
356 g = *(src_address++);
357 b = *(src_address++);
358 a = *(src_address++);
359 *(dst_address++) = rgba(r, g, b, a);
360 }
361 }
362 // RGB
363 else if (png_get_color_type(png, info) == PNG_COLOR_TYPE_RGB) {
364 uint8_t* src_address = rows_pointer[y];
365 uint32_t* dst_address = (uint32_t*)image->getPixelAddress(0, y);
366 unsigned int x, r, g, b, a;
367
368 for (x=0; x<width; x++) {
369 r = *(src_address++);
370 g = *(src_address++);
371 b = *(src_address++);
372
373 // Transparent color
374 if (png_trans_color &&
375 r == png_trans_color->red &&
376 g == png_trans_color->green &&
377 b == png_trans_color->blue) {
378 a = 0;
379 if (!fop->sequenceGetHasAlpha())
380 fop->sequenceSetHasAlpha(true);
381 }
382 else
383 a = 255;
384
385 *(dst_address++) = rgba(r, g, b, a);
386 }
387 }
388 // GRAY_ALPHA
389 else if (png_get_color_type(png, info) == PNG_COLOR_TYPE_GRAY_ALPHA) {
390 uint8_t* src_address = rows_pointer[y];
391 uint16_t* dst_address = (uint16_t*)image->getPixelAddress(0, y);
392 unsigned int x, k, a;
393
394 for (x=0; x<width; x++) {
395 k = *(src_address++);
396 a = *(src_address++);
397 *(dst_address++) = graya(k, a);
398 }
399 }
400 // GRAY
401 else if (png_get_color_type(png, info) == PNG_COLOR_TYPE_GRAY) {
402 uint8_t* src_address = rows_pointer[y];
403 uint16_t* dst_address = (uint16_t*)image->getPixelAddress(0, y);
404 unsigned int x, k, a;
405
406 for (x=0; x<width; x++) {
407 k = *(src_address++);
408
409 // Transparent color
410 if (png_trans_color &&
411 k == png_trans_color->gray) {
412 a = 0;
413 if (!fop->sequenceGetHasAlpha())
414 fop->sequenceSetHasAlpha(true);
415 }
416 else
417 a = 255;
418
419 *(dst_address++) = graya(k, a);
420 }
421 }
422 // PALETTE
423 else if (png_get_color_type(png, info) == PNG_COLOR_TYPE_PALETTE) {
424 uint8_t* src_address = rows_pointer[y];
425 uint8_t* dst_address = (uint8_t*)image->getPixelAddress(0, y);
426 unsigned int x;
427
428 for (x=0; x<width; x++)
429 *(dst_address++) = *(src_address++);
430 }
431 png_free(png, rows_pointer[y]);
432 }
433 png_free(png, rows_pointer);
434
435 // Setup the color space.
436 auto colorSpace = PngFormat::loadColorSpace(png, info);
437 if (colorSpace)
438 fop->setEmbeddedColorProfile();
439 else { // sRGB is the default PNG color space.
440 colorSpace = gfx::ColorSpace::MakeSRGB();
441 }
442 if (colorSpace &&
443 fop->document()->sprite()->colorSpace()->type() == gfx::ColorSpace::None) {
444 fop->document()->sprite()->setColorSpace(colorSpace);
445 fop->document()->notifyColorSpaceChanged();
446 }
447
448 ASSERT(opts != nullptr);
449 if (!opts->isEmpty())
450 fop->setLoadedFormatOptions(opts);
451
452 return true;
453}
454
455// Returns a colorSpace object that represents any
456// color space information in the encoded data. If the encoded data
457// contains an invalid/unsupported color space, this will return
458// NULL. If there is no color space information, it will guess sRGB
459//
460// Code to read color spaces from png files from Skia (SkPngCodec.cpp)
461// by Google Inc.
462gfx::ColorSpaceRef PngFormat::loadColorSpace(png_structp png_ptr, png_infop info_ptr)
463{
464 // First check for an ICC profile
465 png_bytep profile;
466 png_uint_32 length;
467 // The below variables are unused, however, we need to pass them in anyway or
468 // png_get_iCCP() will return nothing.
469 // Could knowing the |name| of the profile ever be interesting? Maybe for debugging?
470 png_charp name;
471 // The |compression| is uninteresting since:
472 // (1) libpng has already decompressed the profile for us.
473 // (2) "deflate" is the only mode of decompression that libpng supports.
474 int compression;
475 if (PNG_INFO_iCCP == png_get_iCCP(png_ptr, info_ptr,
476 &name, &compression,
477 &profile, &length)) {
478 auto colorSpace = gfx::ColorSpace::MakeICC(profile, length);
479 if (name)
480 colorSpace->setName(name);
481 return colorSpace;
482 }
483
484 // Second, check for sRGB.
485 if (png_get_valid(png_ptr, info_ptr, PNG_INFO_sRGB)) {
486 // sRGB chunks also store a rendering intent: Absolute, Relative,
487 // Perceptual, and Saturation.
488 return gfx::ColorSpace::MakeSRGB();
489 }
490
491 // Next, check for chromaticities.
492 png_fixed_point wx, wy, rx, ry, gx, gy, bx, by, invGamma;
493 if (png_get_cHRM_fixed(png_ptr, info_ptr,
494 &wx, &wy, &rx, &ry, &gx, &gy, &bx, &by)) {
495 gfx::ColorSpacePrimaries primaries;
496 primaries.wx = png_fixtof(wx); primaries.wy = png_fixtof(wy);
497 primaries.rx = png_fixtof(rx); primaries.ry = png_fixtof(ry);
498 primaries.gx = png_fixtof(gx); primaries.gy = png_fixtof(gy);
499 primaries.bx = png_fixtof(bx); primaries.by = png_fixtof(by);
500
501 if (PNG_INFO_gAMA == png_get_gAMA_fixed(png_ptr, info_ptr, &invGamma)) {
502 gfx::ColorSpaceTransferFn fn;
503 fn.a = 1.0f;
504 fn.b = fn.c = fn.d = fn.e = fn.f = 0.0f;
505 fn.g = 1.0f / png_fixtof(invGamma);
506
507 return gfx::ColorSpace::MakeRGB(fn, primaries);
508 }
509
510 // Default to sRGB gamma if the image has color space information,
511 // but does not specify gamma.
512 return gfx::ColorSpace::MakeRGBWithSRGBGamma(primaries);
513 }
514
515 // Last, check for gamma.
516 if (PNG_INFO_gAMA == png_get_gAMA_fixed(png_ptr, info_ptr, &invGamma)) {
517 // Since there is no cHRM, we will guess sRGB gamut.
518 return gfx::ColorSpace::MakeSRGBWithGamma(1.0f / png_fixtof(invGamma));
519 }
520
521 // No color space.
522 return nullptr;
523}
524
525#ifdef ENABLE_SAVE
526
527bool PngFormat::onSave(FileOp* fop)
528{
529 png_infop info;
530 png_colorp palette = nullptr;
531 png_bytep row_pointer;
532 int color_type = 0;
533
534 FileHandle handle(open_file_with_exception_sync_on_close(fop->filename(), "wb"));
535 FILE* fp = handle.get();
536
537 png_structp png =
538 png_create_write_struct(PNG_LIBPNG_VER_STRING, (png_voidp)fop,
539 report_png_error, report_png_error);
540 if (png == nullptr)
541 return false;
542
543 // Remove sRGB profile checks
544 png_set_option(png, PNG_SKIP_sRGB_CHECK_PROFILE, PNG_OPTION_ON);
545
546 info = png_create_info_struct(png);
547 DestroyWritePng destroyer(png, info);
548 if (info == nullptr)
549 return false;
550
551 if (setjmp(png_jmpbuf(png)))
552 return false;
553
554 png_init_io(png, fp);
555
556 const FileAbstractImage* img = fop->abstractImage();
557 const ImageSpec spec = img->spec();
558
559 switch (spec.colorMode()) {
560 case ColorMode::RGB:
561 color_type =
562 (img->needAlpha() ||
563 fix_one_alpha_pixel ?
564 PNG_COLOR_TYPE_RGB_ALPHA:
565 PNG_COLOR_TYPE_RGB);
566 break;
567 case ColorMode::GRAYSCALE:
568 color_type =
569 (img->needAlpha() ||
570 fix_one_alpha_pixel ?
571 PNG_COLOR_TYPE_GRAY_ALPHA:
572 PNG_COLOR_TYPE_GRAY);
573 break;
574 case ColorMode::INDEXED:
575 if (fix_one_alpha_pixel)
576 color_type = PNG_COLOR_TYPE_RGB_ALPHA;
577 else
578 color_type = PNG_COLOR_TYPE_PALETTE;
579 break;
580 }
581
582 const png_uint_32 width = spec.width();
583 const png_uint_32 height = spec.height();
584
585 png_set_IHDR(png, info, width, height, 8, color_type,
586 PNG_INTERLACE_NONE, PNG_COMPRESSION_TYPE_BASE, PNG_FILTER_TYPE_BASE);
587
588 // User chunks
589 auto opts = fop->formatOptionsOfDocument<PngOptions>();
590 if (opts && !opts->isEmpty()) {
591 int num_unknowns = opts->size();
592 ASSERT(num_unknowns > 0);
593 std::vector<png_unknown_chunk> unknowns(num_unknowns);
594 int i = 0;
595 for (const auto& chunk : opts->chunks()) {
596 png_unknown_chunk& unknown = unknowns[i];
597 for (int i=0; i<5; ++i) {
598 unknown.name[i] =
599 (i < int(chunk.name.size()) ? chunk.name[i]: 0);
600 }
601 PNG_TRACE("PNG: Write unknown chunk '%c%c%c%c'\n",
602 unknown.name[0],
603 unknown.name[1],
604 unknown.name[2],
605 unknown.name[3]);
606 unknown.data = (png_byte*)&chunk.data[0];
607 unknown.size = chunk.data.size();
608 unknown.location = chunk.location;
609 ++i;
610 }
611 png_set_unknown_chunks(png, info, &unknowns[0], num_unknowns);
612 }
613
614 if (fop->preserveColorProfile() && spec.colorSpace()) {
615 saveColorSpace(png, info, spec.colorSpace().get());
616 }
617
618 if (color_type == PNG_COLOR_TYPE_PALETTE) {
619 int c, r, g, b;
620 int pal_size = fop->sequenceGetNColors();
621 pal_size = std::clamp(pal_size, 1, PNG_MAX_PALETTE_LENGTH);
622
623#if PNG_MAX_PALETTE_LENGTH != 256
624#error PNG_MAX_PALETTE_LENGTH should be 256
625#endif
626
627 // Save the color palette.
628 palette = (png_colorp)png_malloc(png, pal_size * sizeof(png_color));
629 for (c = 0; c < pal_size; c++) {
630 fop->sequenceGetColor(c, &r, &g, &b);
631 palette[c].red = r;
632 palette[c].green = g;
633 palette[c].blue = b;
634 }
635
636 png_set_PLTE(png, info, palette, pal_size);
637
638 // If the sprite does not have a (visible) background layer, we
639 // put alpha=0 to the transparent color.
640 int mask_entry = -1;
641 if (fop->document()->sprite()->backgroundLayer() == nullptr ||
642 !fop->document()->sprite()->backgroundLayer()->isVisible()) {
643 mask_entry = fop->document()->sprite()->transparentColor();
644 }
645
646 bool all_opaque = true;
647 int num_trans = pal_size;
648 png_bytep trans = (png_bytep)png_malloc(png, num_trans);
649
650 for (c=0; c<num_trans; ++c) {
651 int alpha = 255;
652 fop->sequenceGetAlpha(c, &alpha);
653 trans[c] = (c == mask_entry ? 0: alpha);
654 if (alpha < 255)
655 all_opaque = false;
656 }
657
658 if (!all_opaque || mask_entry >= 0)
659 png_set_tRNS(png, info, trans, num_trans, nullptr);
660
661 png_free(png, trans);
662 }
663
664 png_write_info(png, info);
665 png_set_packing(png);
666
667 row_pointer = (png_bytep)png_malloc(png, png_get_rowbytes(png, info));
668
669 for (png_uint_32 y=0; y<height; ++y) {
670 uint8_t* dst_address = row_pointer;
671
672 if (png_get_color_type(png, info) == PNG_COLOR_TYPE_RGB_ALPHA) {
673 unsigned int x, c, a;
674 bool opaque = true;
675
676 if (spec.colorMode() == ColorMode::RGB) {
677 auto src_address = (const uint32_t*)img->getScanline(y);
678
679 for (x=0; x<width; ++x) {
680 c = *(src_address++);
681 a = rgba_geta(c);
682
683 if (opaque) {
684 if (a < 255)
685 opaque = false;
686 else if (fix_one_alpha_pixel && x == width-1 && y == height-1)
687 a = 254;
688 }
689
690 *(dst_address++) = rgba_getr(c);
691 *(dst_address++) = rgba_getg(c);
692 *(dst_address++) = rgba_getb(c);
693 *(dst_address++) = a;
694 }
695 }
696 // In case that we are converting an indexed image to RGB just
697 // to convert one pixel with alpha=254.
698 else if (spec.colorMode() == ColorMode::INDEXED) {
699 auto src_address = (const uint8_t*)img->getScanline(y);
700 unsigned int x, c;
701 int r, g, b, a;
702 bool opaque = true;
703
704 for (x=0; x<width; ++x) {
705 c = *(src_address++);
706 fop->sequenceGetColor(c, &r, &g, &b);
707 fop->sequenceGetAlpha(c, &a);
708
709 if (opaque) {
710 if (a < 255)
711 opaque = false;
712 else if (fix_one_alpha_pixel && x == width-1 && y == height-1)
713 a = 254;
714 }
715
716 *(dst_address++) = r;
717 *(dst_address++) = g;
718 *(dst_address++) = b;
719 *(dst_address++) = a;
720 }
721 }
722 }
723 else if (png_get_color_type(png, info) == PNG_COLOR_TYPE_RGB) {
724 auto src_address = (const uint32_t*)img->getScanline(y);
725 unsigned int x, c;
726
727 for (x=0; x<width; ++x) {
728 c = *(src_address++);
729 *(dst_address++) = rgba_getr(c);
730 *(dst_address++) = rgba_getg(c);
731 *(dst_address++) = rgba_getb(c);
732 }
733 }
734 else if (png_get_color_type(png, info) == PNG_COLOR_TYPE_GRAY_ALPHA) {
735 auto src_address = (const uint16_t*)img->getScanline(y);
736 unsigned int x, c, a;
737 bool opaque = true;
738
739 for (x=0; x<width; x++) {
740 c = *(src_address++);
741 a = graya_geta(c);
742
743 if (opaque) {
744 if (a < 255)
745 opaque = false;
746 else if (fix_one_alpha_pixel && x == width-1 && y == height-1)
747 a = 254;
748 }
749
750 *(dst_address++) = graya_getv(c);
751 *(dst_address++) = a;
752 }
753 }
754 else if (png_get_color_type(png, info) == PNG_COLOR_TYPE_GRAY) {
755 auto src_address = (const uint16_t*)img->getScanline(y);
756 unsigned int x, c;
757
758 for (x=0; x<width; ++x) {
759 c = *(src_address++);
760 *(dst_address++) = graya_getv(c);
761 }
762 }
763 else if (png_get_color_type(png, info) == PNG_COLOR_TYPE_PALETTE) {
764 auto src_address = (const uint8_t*)img->getScanline(y);
765 unsigned int x;
766
767 for (x=0; x<width; ++x)
768 *(dst_address++) = *(src_address++);
769 }
770
771 png_write_rows(png, &row_pointer, 1);
772
773 fop->setProgress((double)(y+1) / (double)(height));
774 }
775
776 png_free(png, row_pointer);
777 png_write_end(png, info);
778
779 if (spec.colorMode() == ColorMode::INDEXED) {
780 png_free(png, palette);
781 palette = nullptr;
782 }
783
784 return true;
785}
786
787void PngFormat::saveColorSpace(png_structp png_ptr, png_infop info_ptr,
788 const gfx::ColorSpace* colorSpace)
789{
790 switch (colorSpace->type()) {
791
792 case gfx::ColorSpace::None:
793 // Do just nothing (png file without profile, like old Aseprite versions)
794 break;
795
796 case gfx::ColorSpace::sRGB:
797 // TODO save the original intent
798 if (!colorSpace->hasGamma()) {
799 png_set_sRGB(png_ptr, info_ptr, PNG_sRGB_INTENT_PERCEPTUAL);
800 return;
801 }
802
803 // Continue to RGB case...
804 [[fallthrough]];
805
806 case gfx::ColorSpace::RGB: {
807 if (colorSpace->hasPrimaries()) {
808 const gfx::ColorSpacePrimaries* p = colorSpace->primaries();
809 png_set_cHRM_fixed(png_ptr, info_ptr,
810 png_ftofix(p->wx), png_ftofix(p->wy),
811 png_ftofix(p->rx), png_ftofix(p->ry),
812 png_ftofix(p->gx), png_ftofix(p->gy),
813 png_ftofix(p->bx), png_ftofix(p->by));
814 }
815 if (colorSpace->hasGamma()) {
816 png_set_gAMA_fixed(png_ptr, info_ptr,
817 png_ftofix(1.0f / colorSpace->gamma()));
818 }
819 break;
820 }
821
822 case gfx::ColorSpace::ICC: {
823 png_set_iCCP(png_ptr, info_ptr,
824 (png_const_charp)colorSpace->name().c_str(),
825 PNG_COMPRESSION_TYPE_DEFAULT,
826 (png_const_bytep)colorSpace->iccData(),
827 (png_uint_32)colorSpace->iccSize());
828 break;
829 }
830
831 }
832}
833
834#endif // ENABLE_SAVE
835
836} // namespace app
837