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/console.h" |
14 | #include "app/context.h" |
15 | #include "app/doc.h" |
16 | #include "app/file/file.h" |
17 | #include "app/file/file_format.h" |
18 | #include "app/file/format_options.h" |
19 | #include "app/find_widget.h" |
20 | #include "app/load_widget.h" |
21 | #include "app/pref/preferences.h" |
22 | #include "base/file_handle.h" |
23 | #include "base/memory.h" |
24 | #include "doc/doc.h" |
25 | |
26 | #include <algorithm> |
27 | #include <csetjmp> |
28 | #include <cstdio> |
29 | #include <cstdlib> |
30 | |
31 | #include "jpeg_options.xml.h" |
32 | |
33 | #include "jpeglib.h" |
34 | |
35 | namespace app { |
36 | |
37 | using namespace base; |
38 | |
39 | class JpegFormat : public FileFormat { |
40 | // Data for JPEG files |
41 | class JpegOptions : public FormatOptions { |
42 | public: |
43 | JpegOptions() : quality(1.0f) { } // 1.0 maximum quality. |
44 | float quality; |
45 | }; |
46 | |
47 | const char* onGetName() const override { |
48 | return "jpeg" ; |
49 | } |
50 | |
51 | void onGetExtensions(base::paths& exts) const override { |
52 | exts.push_back("jpeg" ); |
53 | exts.push_back("jpg" ); |
54 | } |
55 | |
56 | dio::FileFormat onGetDioFormat() const override { |
57 | return dio::FileFormat::JPEG_IMAGE; |
58 | } |
59 | |
60 | int onGetFlags() const override { |
61 | return |
62 | FILE_SUPPORT_LOAD | |
63 | FILE_SUPPORT_SAVE | |
64 | FILE_SUPPORT_RGB | |
65 | FILE_SUPPORT_GRAY | |
66 | FILE_SUPPORT_SEQUENCES | |
67 | FILE_SUPPORT_GET_FORMAT_OPTIONS | |
68 | FILE_ENCODE_ABSTRACT_IMAGE; |
69 | } |
70 | |
71 | bool onLoad(FileOp* fop) override; |
72 | gfx::ColorSpaceRef loadColorSpace(FileOp* fop, jpeg_decompress_struct* dinfo); |
73 | #ifdef ENABLE_SAVE |
74 | bool onSave(FileOp* fop) override; |
75 | void saveColorSpace(FileOp* fop, jpeg_compress_struct* cinfo, |
76 | const gfx::ColorSpace* colorSpace); |
77 | #endif |
78 | |
79 | FormatOptionsPtr onAskUserForFormatOptions(FileOp* fop) override; |
80 | }; |
81 | |
82 | FileFormat* CreateJpegFormat() |
83 | { |
84 | return new JpegFormat; |
85 | } |
86 | |
87 | struct error_mgr { |
88 | struct jpeg_error_mgr head; |
89 | jmp_buf setjmp_buffer; |
90 | FileOp* fop; |
91 | }; |
92 | |
93 | static void error_exit(j_common_ptr cinfo) |
94 | { |
95 | // Display the message. |
96 | (*cinfo->err->output_message)(cinfo); |
97 | |
98 | // Return control to the setjmp point. |
99 | longjmp(((struct error_mgr *)cinfo->err)->setjmp_buffer, 1); |
100 | } |
101 | |
102 | static void output_message(j_common_ptr cinfo) |
103 | { |
104 | char buffer[JMSG_LENGTH_MAX]; |
105 | |
106 | // Format the message. |
107 | (*cinfo->err->format_message)(cinfo, buffer); |
108 | |
109 | // Put in the log file if. |
110 | LOG(ERROR, "JPEG: \"%s\"\n" , buffer); |
111 | |
112 | // Leave the message for the application. |
113 | ((struct error_mgr *)cinfo->err)->fop->setError("%s\n" , buffer); |
114 | } |
115 | |
116 | // Some code to read color spaces from jpeg files is from Skia |
117 | // (SkJpegCodec.cpp) by Google Inc. |
118 | static constexpr uint32_t kMarkerMaxSize = 65533; |
119 | static constexpr uint32_t kICCMarker = JPEG_APP0 + 2; |
120 | static constexpr uint32_t = 14; |
121 | static constexpr uint32_t kICCAvailDataPerMarker = (kMarkerMaxSize - kICCMarkerHeaderSize); |
122 | static constexpr uint8_t kICCSig[] = { 'I', 'C', 'C', '_', 'P', 'R', 'O', 'F', 'I', 'L', 'E', '\0' }; |
123 | |
124 | static bool is_icc_marker(jpeg_marker_struct* marker) |
125 | { |
126 | if (kICCMarker != marker->marker || |
127 | marker->data_length < kICCMarkerHeaderSize) { |
128 | return false; |
129 | } |
130 | else |
131 | return !memcmp(marker->data, kICCSig, sizeof(kICCSig)); |
132 | } |
133 | |
134 | bool JpegFormat::onLoad(FileOp* fop) |
135 | { |
136 | struct jpeg_decompress_struct dinfo; |
137 | struct error_mgr jerr; |
138 | JDIMENSION num_scanlines; |
139 | JSAMPARRAY buffer; |
140 | JDIMENSION buffer_height; |
141 | int c; |
142 | |
143 | FileHandle handle(open_file_with_exception(fop->filename(), "rb" )); |
144 | FILE* file = handle.get(); |
145 | |
146 | // Initialize the JPEG decompression object with error handling. |
147 | jerr.fop = fop; |
148 | dinfo.err = jpeg_std_error(&jerr.head); |
149 | |
150 | jerr.head.error_exit = error_exit; |
151 | jerr.head.output_message = output_message; |
152 | |
153 | // Establish the setjmp return context for error_exit to use. |
154 | if (setjmp(jerr.setjmp_buffer)) { |
155 | jpeg_destroy_decompress(&dinfo); |
156 | return false; |
157 | } |
158 | |
159 | jpeg_create_decompress(&dinfo); |
160 | |
161 | // Specify data source for decompression. |
162 | jpeg_stdio_src(&dinfo, file); |
163 | |
164 | // Instruct jpeg library to save the markers that we care |
165 | // about. Since the color profile will not change, we can skip this |
166 | // step on rewinds. |
167 | jpeg_save_markers(&dinfo, kICCMarker, 0xFFFF); |
168 | |
169 | // Read file header, set default decompression parameters. |
170 | jpeg_read_header(&dinfo, true); |
171 | |
172 | if (dinfo.jpeg_color_space == JCS_GRAYSCALE) |
173 | dinfo.out_color_space = JCS_GRAYSCALE; |
174 | else |
175 | dinfo.out_color_space = JCS_RGB; |
176 | |
177 | // Start decompressor. |
178 | jpeg_start_decompress(&dinfo); |
179 | |
180 | // Create the image. |
181 | ImageRef image = fop->sequenceImage( |
182 | (dinfo.out_color_space == JCS_RGB ? IMAGE_RGB: |
183 | IMAGE_GRAYSCALE), |
184 | dinfo.output_width, |
185 | dinfo.output_height); |
186 | if (!image) { |
187 | jpeg_destroy_decompress(&dinfo); |
188 | return false; |
189 | } |
190 | |
191 | // Create the buffer. |
192 | buffer_height = dinfo.rec_outbuf_height; |
193 | buffer = (JSAMPARRAY)base_malloc(sizeof(JSAMPROW) * buffer_height); |
194 | if (!buffer) { |
195 | jpeg_destroy_decompress(&dinfo); |
196 | return false; |
197 | } |
198 | |
199 | for (c=0; c<(int)buffer_height; c++) { |
200 | buffer[c] = (JSAMPROW)base_malloc(sizeof(JSAMPLE) * |
201 | dinfo.output_width * dinfo.output_components); |
202 | if (!buffer[c]) { |
203 | for (c--; c>=0; c--) |
204 | base_free(buffer[c]); |
205 | base_free(buffer); |
206 | jpeg_destroy_decompress(&dinfo); |
207 | return false; |
208 | } |
209 | } |
210 | |
211 | // Generate a grayscale palette if is necessary. |
212 | if (image->pixelFormat() == IMAGE_GRAYSCALE) |
213 | for (c=0; c<256; c++) |
214 | fop->sequenceSetColor(c, c, c, c); |
215 | |
216 | // Read each scan line. |
217 | while (dinfo.output_scanline < dinfo.output_height) { |
218 | num_scanlines = jpeg_read_scanlines(&dinfo, buffer, buffer_height); |
219 | |
220 | // RGB |
221 | if (image->pixelFormat() == IMAGE_RGB) { |
222 | uint8_t* src_address; |
223 | uint32_t* dst_address; |
224 | int x, y, r, g, b; |
225 | |
226 | for (y=0; y<(int)num_scanlines; y++) { |
227 | src_address = ((uint8_t**)buffer)[y]; |
228 | dst_address = (uint32_t*)image->getPixelAddress(0, dinfo.output_scanline-1+y); |
229 | |
230 | for (x=0; x<image->width(); x++) { |
231 | r = *(src_address++); |
232 | g = *(src_address++); |
233 | b = *(src_address++); |
234 | *(dst_address++) = rgba(r, g, b, 255); |
235 | } |
236 | } |
237 | } |
238 | // Grayscale |
239 | else { |
240 | uint8_t* src_address; |
241 | uint16_t* dst_address; |
242 | int x, y; |
243 | |
244 | for (y=0; y<(int)num_scanlines; y++) { |
245 | src_address = ((uint8_t**)buffer)[y]; |
246 | dst_address = (uint16_t*)image->getPixelAddress(0, dinfo.output_scanline-1+y); |
247 | |
248 | for (x=0; x<image->width(); x++) |
249 | *(dst_address++) = graya(*(src_address++), 255); |
250 | } |
251 | } |
252 | |
253 | fop->setProgress((float)(dinfo.output_scanline+1) / (float)(dinfo.output_height)); |
254 | if (fop->isStop()) |
255 | break; |
256 | } |
257 | |
258 | // Read color space |
259 | gfx::ColorSpaceRef colorSpace = loadColorSpace(fop, &dinfo); |
260 | if (colorSpace) |
261 | fop->setEmbeddedColorProfile(); |
262 | else { // sRGB is the default JPG color space. |
263 | colorSpace = gfx::ColorSpace::MakeSRGB(); |
264 | } |
265 | if (colorSpace && |
266 | fop->document()->sprite()->colorSpace()->type() == gfx::ColorSpace::None) { |
267 | fop->document()->sprite()->setColorSpace(colorSpace); |
268 | fop->document()->notifyColorSpaceChanged(); |
269 | } |
270 | |
271 | for (c=0; c<(int)buffer_height; c++) |
272 | base_free(buffer[c]); |
273 | base_free(buffer); |
274 | |
275 | jpeg_finish_decompress(&dinfo); |
276 | jpeg_destroy_decompress(&dinfo); |
277 | |
278 | return true; |
279 | } |
280 | |
281 | // ICC profiles may be stored using a sequence of multiple markers. We obtain the ICC profile |
282 | // in two steps: |
283 | // (1) Discover all ICC profile markers and verify that they are numbered properly. |
284 | // (2) Copy the data from each marker into a contiguous ICC profile. |
285 | gfx::ColorSpaceRef JpegFormat::loadColorSpace(FileOp* fop, jpeg_decompress_struct* dinfo) |
286 | { |
287 | // Note that 256 will be enough storage space since each markerIndex is stored in 8-bits. |
288 | jpeg_marker_struct* markerSequence[256]; |
289 | memset(markerSequence, 0, sizeof(markerSequence)); |
290 | uint8_t numMarkers = 0; |
291 | size_t totalBytes = 0; |
292 | |
293 | // Discover any ICC markers and verify that they are numbered properly. |
294 | for (jpeg_marker_struct* marker = dinfo->marker_list; marker; marker = marker->next) { |
295 | if (is_icc_marker(marker)) { |
296 | // Verify that numMarkers is valid and consistent. |
297 | if (0 == numMarkers) { |
298 | numMarkers = marker->data[13]; |
299 | if (0 == numMarkers) { |
300 | fop->setError("ICC Profile Error: numMarkers must be greater than zero.\n" ); |
301 | return nullptr; |
302 | } |
303 | } |
304 | else if (numMarkers != marker->data[13]) { |
305 | fop->setError("ICC Profile Error: numMarkers must be consistent.\n" ); |
306 | return nullptr; |
307 | } |
308 | |
309 | // Verify that the markerIndex is valid and unique. Note that zero is not |
310 | // a valid index. |
311 | uint8_t markerIndex = marker->data[12]; |
312 | if (markerIndex == 0 || markerIndex > numMarkers) { |
313 | fop->setError("ICC Profile Error: markerIndex is invalid.\n" ); |
314 | return nullptr; |
315 | } |
316 | if (markerSequence[markerIndex]) { |
317 | fop->setError("ICC Profile Error: Duplicate value of markerIndex.\n" ); |
318 | return nullptr; |
319 | } |
320 | markerSequence[markerIndex] = marker; |
321 | ASSERT(marker->data_length >= kICCMarkerHeaderSize); |
322 | totalBytes += marker->data_length - kICCMarkerHeaderSize; |
323 | } |
324 | } |
325 | |
326 | if (0 == totalBytes) { |
327 | // No non-empty ICC profile markers were found. |
328 | return nullptr; |
329 | } |
330 | |
331 | // Combine the ICC marker data into a contiguous profile. |
332 | std::vector<uint8_t> iccData(totalBytes); |
333 | uint8_t* dst = &iccData[0]; |
334 | for (uint32_t i = 1; i <= numMarkers; i++) { |
335 | jpeg_marker_struct* marker = markerSequence[i]; |
336 | if (!marker) { |
337 | fop->setError("ICC Profile Error: Missing marker %d of %d.\n" , i, numMarkers); |
338 | return nullptr; |
339 | } |
340 | |
341 | uint8_t* src = ((uint8_t*)marker->data) + kICCMarkerHeaderSize; |
342 | size_t bytes = marker->data_length - kICCMarkerHeaderSize; |
343 | memcpy(dst, src, bytes); |
344 | dst = dst + bytes; |
345 | } |
346 | |
347 | return gfx::ColorSpace::MakeICC(std::move(iccData)); |
348 | } |
349 | |
350 | #ifdef ENABLE_SAVE |
351 | |
352 | bool JpegFormat::onSave(FileOp* fop) |
353 | { |
354 | struct jpeg_compress_struct cinfo; |
355 | struct error_mgr jerr; |
356 | const FileAbstractImage* img = fop->abstractImage(); |
357 | const ImageSpec spec = img->spec(); |
358 | JSAMPARRAY buffer; |
359 | JDIMENSION buffer_height; |
360 | const auto jpeg_options = std::static_pointer_cast<JpegOptions>(fop->formatOptions()); |
361 | const int qualityValue = |
362 | (jpeg_options ? (int)std::clamp(100.0f * jpeg_options->quality, 0.f, 100.f): |
363 | 100); |
364 | |
365 | int c; |
366 | |
367 | LOG("JPEG: Saving with options: quality=%d\n" , qualityValue); |
368 | |
369 | // Open the file for write in it. |
370 | FileHandle handle(open_file_with_exception_sync_on_close(fop->filename(), "wb" )); |
371 | FILE* file = handle.get(); |
372 | |
373 | // Allocate and initialize JPEG compression object. |
374 | jerr.fop = fop; |
375 | cinfo.err = jpeg_std_error(&jerr.head); |
376 | jpeg_create_compress(&cinfo); |
377 | |
378 | // SPECIFY data destination file. |
379 | jpeg_stdio_dest(&cinfo, file); |
380 | |
381 | // SET parameters for compression. |
382 | cinfo.image_width = spec.width(); |
383 | cinfo.image_height = spec.height(); |
384 | |
385 | if (spec.colorMode() == ColorMode::GRAYSCALE) { |
386 | cinfo.input_components = 1; |
387 | cinfo.in_color_space = JCS_GRAYSCALE; |
388 | } |
389 | else { |
390 | cinfo.input_components = 3; |
391 | cinfo.in_color_space = JCS_RGB; |
392 | } |
393 | |
394 | jpeg_set_defaults(&cinfo); |
395 | jpeg_set_quality(&cinfo, qualityValue, true); |
396 | cinfo.dct_method = JDCT_ISLOW; |
397 | cinfo.smoothing_factor = 0; |
398 | |
399 | // START compressor. |
400 | jpeg_start_compress(&cinfo, true); |
401 | |
402 | // Save color space |
403 | if (fop->preserveColorProfile() && |
404 | fop->document()->sprite()->colorSpace()) |
405 | saveColorSpace(fop, &cinfo, fop->document()->sprite()->colorSpace().get()); |
406 | |
407 | // CREATE the buffer. |
408 | buffer_height = 1; |
409 | buffer = (JSAMPARRAY)base_malloc(sizeof(JSAMPROW) * buffer_height); |
410 | if (!buffer) { |
411 | fop->setError("Not enough memory for the buffer.\n" ); |
412 | jpeg_destroy_compress(&cinfo); |
413 | return false; |
414 | } |
415 | |
416 | for (c=0; c<(int)buffer_height; c++) { |
417 | buffer[c] = (JSAMPROW)base_malloc(sizeof(JSAMPLE) * |
418 | cinfo.image_width * cinfo.num_components); |
419 | if (!buffer[c]) { |
420 | fop->setError("Not enough memory for buffer scanlines.\n" ); |
421 | for (c--; c>=0; c--) |
422 | base_free(buffer[c]); |
423 | base_free(buffer); |
424 | jpeg_destroy_compress(&cinfo); |
425 | return false; |
426 | } |
427 | } |
428 | |
429 | // Write each scan line. |
430 | while (cinfo.next_scanline < cinfo.image_height) { |
431 | // RGB |
432 | if (spec.colorMode() == ColorMode::RGB) { |
433 | uint32_t* src_address; |
434 | uint8_t* dst_address; |
435 | int x, y; |
436 | for (y=0; y<(int)buffer_height; y++) { |
437 | src_address = (uint32_t*)img->getScanline(cinfo.next_scanline+y); |
438 | dst_address = ((uint8_t**)buffer)[y]; |
439 | |
440 | for (x=0; x<spec.width(); ++x) { |
441 | c = *(src_address++); |
442 | *(dst_address++) = rgba_getr(c); |
443 | *(dst_address++) = rgba_getg(c); |
444 | *(dst_address++) = rgba_getb(c); |
445 | } |
446 | } |
447 | } |
448 | // Grayscale. |
449 | else { |
450 | uint16_t* src_address; |
451 | uint8_t* dst_address; |
452 | int x, y; |
453 | for (y=0; y<(int)buffer_height; y++) { |
454 | src_address = (uint16_t*)img->getScanline(cinfo.next_scanline+y); |
455 | dst_address = ((uint8_t**)buffer)[y]; |
456 | for (x=0; x<spec.width(); ++x) |
457 | *(dst_address++) = graya_getv(*(src_address++)); |
458 | } |
459 | } |
460 | jpeg_write_scanlines(&cinfo, buffer, buffer_height); |
461 | |
462 | fop->setProgress((float)(cinfo.next_scanline+1) / (float)(cinfo.image_height)); |
463 | } |
464 | |
465 | // Destroy all data. |
466 | for (c=0; c<(int)buffer_height; c++) |
467 | base_free(buffer[c]); |
468 | base_free(buffer); |
469 | |
470 | // Finish compression. |
471 | jpeg_finish_compress(&cinfo); |
472 | |
473 | // Release JPEG compression object. |
474 | jpeg_destroy_compress(&cinfo); |
475 | |
476 | // All fine. |
477 | return true; |
478 | } |
479 | |
480 | void JpegFormat::saveColorSpace(FileOp* fop, jpeg_compress_struct* cinfo, |
481 | const gfx::ColorSpace* colorSpace) |
482 | { |
483 | if (!colorSpace || colorSpace->type() != gfx::ColorSpace::ICC) |
484 | return; |
485 | |
486 | size_t iccSize = colorSpace->iccSize(); |
487 | auto iccData = (const uint8_t*)colorSpace->iccData(); |
488 | if (!iccSize || !iccData) |
489 | return; |
490 | |
491 | std::vector<uint8_t> markerData(kMarkerMaxSize); |
492 | int markerIndex = 1; |
493 | int numMarkers = |
494 | (iccSize / kICCAvailDataPerMarker) + |
495 | (iccSize % kICCAvailDataPerMarker > 0 ? 1: 0); |
496 | |
497 | // ICC profile too big to fit in JPEG markers (64kb*255 ~= 16mb) |
498 | if (numMarkers > 255) { |
499 | fop->setError("ICC profile is too big to enter in the JPEG file.\n" ); |
500 | return; |
501 | } |
502 | |
503 | while (iccSize > 0) { |
504 | const size_t n = std::min<int>(iccSize, kICCAvailDataPerMarker); |
505 | |
506 | ASSERT(n > 0); |
507 | ASSERT(n < kICCAvailDataPerMarker); |
508 | |
509 | // Marker Header |
510 | std::copy(kICCSig, kICCSig+sizeof(kICCSig), &markerData[0]); |
511 | markerData[sizeof(kICCSig) ] = markerIndex; |
512 | markerData[sizeof(kICCSig)+1] = numMarkers; |
513 | |
514 | // Marker Data |
515 | std::copy(iccData, iccData+n, &markerData[kICCMarkerHeaderSize]); |
516 | |
517 | jpeg_write_marker(cinfo, kICCMarker, &markerData[0], kICCMarkerHeaderSize + n); |
518 | |
519 | ++markerIndex; |
520 | iccSize -= n; |
521 | iccData += n; |
522 | } |
523 | } |
524 | |
525 | #endif // ENABLE_SAVE |
526 | |
527 | // Shows the JPEG configuration dialog. |
528 | FormatOptionsPtr JpegFormat::onAskUserForFormatOptions(FileOp* fop) |
529 | { |
530 | auto opts = fop->formatOptionsOfDocument<JpegOptions>(); |
531 | #ifdef ENABLE_UI |
532 | if (fop->context() && fop->context()->isUIAvailable()) { |
533 | try { |
534 | auto& pref = Preferences::instance(); |
535 | |
536 | if (pref.isSet(pref.jpeg.quality)) |
537 | opts->quality = pref.jpeg.quality(); |
538 | |
539 | if (pref.jpeg.showAlert()) { |
540 | app::gen::JpegOptions win; |
541 | win.quality()->setValue(int(opts->quality * 10.0f)); |
542 | win.openWindowInForeground(); |
543 | |
544 | if (win.closer() == win.ok()) { |
545 | pref.jpeg.quality(float(win.quality()->getValue()) / 10.0f); |
546 | pref.jpeg.showAlert(!win.dontShow()->isSelected()); |
547 | |
548 | opts->quality = pref.jpeg.quality(); |
549 | } |
550 | else { |
551 | opts.reset(); |
552 | } |
553 | } |
554 | } |
555 | catch (std::exception& e) { |
556 | Console::showException(e); |
557 | return std::shared_ptr<JpegOptions>(0); |
558 | } |
559 | } |
560 | #endif // ENABLE_UI |
561 | return opts; |
562 | } |
563 | |
564 | } // namespace app |
565 | |