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 | |
30 | namespace app { |
31 | |
32 | using namespace base; |
33 | |
34 | class 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 | |
69 | FileFormat* CreatePngFormat() |
70 | { |
71 | return new PngFormat; |
72 | } |
73 | |
74 | static 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() |
80 | static bool fix_one_alpha_pixel = false; |
81 | |
82 | PngEncoderOneAlphaPixel::PngEncoderOneAlphaPixel(bool state) |
83 | { |
84 | fix_one_alpha_pixel = state; |
85 | } |
86 | |
87 | PngEncoderOneAlphaPixel::~PngEncoderOneAlphaPixel() |
88 | { |
89 | fix_one_alpha_pixel = false; |
90 | } |
91 | |
92 | namespace { |
93 | |
94 | class DestroyReadPng { |
95 | png_structp png; |
96 | png_infop info; |
97 | public: |
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 | |
104 | class DestroyWritePng { |
105 | png_structp png; |
106 | png_infop info; |
107 | public: |
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 |
115 | float 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 | |
124 | png_fixed_point png_ftofix(float x) |
125 | { |
126 | return x * 100000.0f; |
127 | } |
128 | |
129 | int 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 | |
157 | bool 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. |
462 | gfx::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 | |
527 | bool 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 | |
787 | void 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 | |