1 | //======================================================================== |
2 | // |
3 | // ImageEmbeddingUtils.cc |
4 | // |
5 | // Copyright (C) 2021 Georgiy Sgibnev <georgiy@sgibnev.com>. Work sponsored by lab50.net. |
6 | // Copyright (C) 2021, 2022 Albert Astals Cid <aacid@kde.org> |
7 | // Copyright (C) 2021 Marco Genasci <fedeliallalinea@gmail.com> |
8 | // Copyright (C) 2023 Jordan Abrahams-Whitehead <ajordanr@google.com> |
9 | // Copyright (C) 2024 g10 Code GmbH, Author: Sune Stolborg Vuorela <sune@vuorela.dk> |
10 | // |
11 | // This file is licensed under the GPLv2 or later |
12 | // |
13 | //======================================================================== |
14 | |
15 | #include <config.h> |
16 | |
17 | #include <memory> |
18 | #ifdef ENABLE_LIBJPEG |
19 | # include <cstdio> |
20 | extern "C" { |
21 | # include <jpeglib.h> |
22 | } |
23 | # include <csetjmp> |
24 | #endif |
25 | #ifdef ENABLE_LIBPNG |
26 | # include <png.h> |
27 | #endif |
28 | |
29 | #include "ImageEmbeddingUtils.h" |
30 | #include "goo/gmem.h" |
31 | #include "goo/GooCheckedOps.h" |
32 | #include "Object.h" |
33 | #include "Array.h" |
34 | #include "Error.h" |
35 | #include "PDFDoc.h" |
36 | |
37 | namespace ImageEmbeddingUtils { |
38 | |
39 | static const uint8_t PNG_MAGIC_NUM[] = { 0x89, 0x50, 0x4e, 0x47 }; |
40 | static const uint8_t JPEG_MAGIC_NUM[] = { 0xff, 0xd8, 0xff }; |
41 | static const uint8_t JPEG2000_MAGIC_NUM[] = { 0x00, 0x00, 0x00, 0x0c, 0x6a, 0x50, 0x20, 0x20 }; |
42 | static const Goffset MAX_MAGIC_NUM_SIZE = sizeof(JPEG2000_MAGIC_NUM); |
43 | |
44 | static bool checkMagicNum(const uint8_t *fileContent, const uint8_t *magicNum, const uint8_t size) |
45 | { |
46 | return (memcmp(s1: fileContent, s2: magicNum, n: size) == 0); |
47 | } |
48 | |
49 | // Transforms an image to XObject. |
50 | class ImageEmbedder |
51 | { |
52 | protected: |
53 | static constexpr const char *DEVICE_GRAY = "DeviceGray" ; |
54 | static constexpr const char *DEVICE_RGB = "DeviceRGB" ; |
55 | |
56 | int m_width; |
57 | int m_height; |
58 | |
59 | ImageEmbedder(const int width, const int height) : m_width(width), m_height(height) { } |
60 | |
61 | // Creates an object of type XObject. You own the returned ptr. |
62 | static Dict *createImageDict(XRef *xref, const char *colorSpace, const int width, const int height, const int bitsPerComponent) |
63 | { |
64 | Dict *imageDict = new Dict(xref); |
65 | imageDict->add(key: "Type" , val: Object(objName, "XObject" )); |
66 | imageDict->add(key: "Subtype" , val: Object(objName, "Image" )); |
67 | imageDict->add(key: "ColorSpace" , val: Object(objName, colorSpace)); |
68 | imageDict->add(key: "Width" , val: Object(width)); |
69 | imageDict->add(key: "Height" , val: Object(height)); |
70 | imageDict->add(key: "BitsPerComponent" , val: Object(bitsPerComponent)); |
71 | return imageDict; |
72 | } |
73 | |
74 | public: |
75 | ImageEmbedder() = delete; |
76 | ImageEmbedder(const ImageEmbedder &) = delete; |
77 | ImageEmbedder &operator=(const ImageEmbedder &) = delete; |
78 | virtual ~ImageEmbedder(); |
79 | |
80 | // Call it only once. |
81 | // Returns ref to a new object or Ref::INVALID. |
82 | virtual Ref embedImage(XRef *xref) = 0; |
83 | }; |
84 | |
85 | ImageEmbedder::~ImageEmbedder() { } |
86 | |
87 | #ifdef ENABLE_LIBPNG |
88 | // Transforms a PNG image to XObject. |
89 | class PngEmbedder : public ImageEmbedder |
90 | { |
91 | // LibpngInputStream is a simple replacement for GInputStream. |
92 | // Used with png_set_read_fn(). |
93 | class LibpngInputStream |
94 | { |
95 | std::unique_ptr<uint8_t[]> m_fileContent; |
96 | uint8_t *m_iterator; |
97 | png_size_t m_remainingSize; |
98 | |
99 | void read(png_bytep out, const png_size_t size) |
100 | { |
101 | const png_size_t fixedSize = (m_remainingSize >= size) ? size : m_remainingSize; |
102 | memcpy(dest: out, src: m_iterator, n: fixedSize); |
103 | m_iterator += fixedSize; |
104 | m_remainingSize -= fixedSize; |
105 | } |
106 | |
107 | public: |
108 | LibpngInputStream(std::unique_ptr<uint8_t[]> &&fileContent, const Goffset size) : m_fileContent(std::move(fileContent)), m_iterator(m_fileContent.get()), m_remainingSize(size) { } |
109 | LibpngInputStream() = delete; |
110 | LibpngInputStream(const LibpngInputStream &) = delete; |
111 | LibpngInputStream &operator=(const LibpngInputStream &) = delete; |
112 | ~LibpngInputStream() = default; |
113 | |
114 | // Pass this static function to png_set_read_fn(). |
115 | static void readCallback(png_structp png, png_bytep out, png_size_t size) |
116 | { |
117 | LibpngInputStream *stream = (LibpngInputStream *)png_get_io_ptr(png_ptr: png); |
118 | if (stream) { |
119 | stream->read(out, size); |
120 | } |
121 | } |
122 | }; |
123 | |
124 | png_structp m_png; |
125 | png_infop m_info; |
126 | LibpngInputStream *m_stream; |
127 | const png_byte m_type; |
128 | const bool m_hasAlpha; |
129 | // Number of color channels. |
130 | const png_byte m_n; |
131 | // Number of color channels excluding alpha channel. Should be 1 or 3. |
132 | const png_byte m_nWithoutAlpha; |
133 | // Shold be 8 or 16. |
134 | const png_byte m_bitDepth; |
135 | // Should be 1 or 2. |
136 | const png_byte m_byteDepth; |
137 | |
138 | PngEmbedder(png_structp png, png_infop info, LibpngInputStream *stream) |
139 | : ImageEmbedder(png_get_image_width(png_ptr: png, info_ptr: info), png_get_image_height(png_ptr: png, info_ptr: info)), |
140 | m_png(png), |
141 | m_info(info), |
142 | m_stream(stream), |
143 | m_type(png_get_color_type(png_ptr: m_png, info_ptr: m_info)), |
144 | m_hasAlpha(m_type & PNG_COLOR_MASK_ALPHA), |
145 | m_n(png_get_channels(png_ptr: m_png, info_ptr: m_info)), |
146 | m_nWithoutAlpha(m_hasAlpha ? m_n - 1 : m_n), |
147 | m_bitDepth(png_get_bit_depth(png_ptr: m_png, info_ptr: m_info)), |
148 | m_byteDepth(m_bitDepth / 8) |
149 | { |
150 | } |
151 | |
152 | // Reads pixels into mainBuffer (RGB/gray channels) and maskBuffer (alpha channel). |
153 | void readPixels(png_bytep mainBuffer, png_bytep maskBuffer) |
154 | { |
155 | // Read pixels from m_png. |
156 | const int rowSize = png_get_rowbytes(png_ptr: m_png, info_ptr: m_info); |
157 | png_bytepp pixels = new png_bytep[m_height]; |
158 | for (int y = 0; y < m_height; y++) { |
159 | pixels[y] = new png_byte[rowSize]; |
160 | } |
161 | png_read_image(png_ptr: m_png, image: pixels); |
162 | |
163 | // Copy pixels into mainBuffer and maskBuffer. |
164 | const png_byte pixelSizeWithoutAlpha = m_nWithoutAlpha * m_byteDepth; |
165 | for (int y = 0; y < m_height; y++) { |
166 | png_bytep row = pixels[y]; |
167 | for (int x = 0; x < m_width; x++) { |
168 | memcpy(dest: mainBuffer, src: row, n: pixelSizeWithoutAlpha); |
169 | mainBuffer += pixelSizeWithoutAlpha; |
170 | row += pixelSizeWithoutAlpha; |
171 | if (m_hasAlpha) { |
172 | memcpy(dest: maskBuffer, src: row, n: m_byteDepth); |
173 | maskBuffer += m_byteDepth; |
174 | row += m_byteDepth; |
175 | } |
176 | } |
177 | } |
178 | |
179 | // Cleanup. |
180 | for (int y = 0; y < m_height; y++) { |
181 | delete[] pixels[y]; |
182 | } |
183 | delete[] pixels; |
184 | } |
185 | |
186 | // Supportive function for create(). |
187 | // We don't want to deal with palette images. |
188 | // We don't want to deal with 1/2/4-bit samples. |
189 | static void fixPng(png_structp png, png_infop info) |
190 | { |
191 | const png_byte colorType = png_get_color_type(png_ptr: png, info_ptr: info); |
192 | const png_byte bitDepth = png_get_bit_depth(png_ptr: png, info_ptr: info); |
193 | |
194 | bool updateRequired = false; |
195 | if (colorType == PNG_COLOR_TYPE_PALETTE) { |
196 | png_set_palette_to_rgb(png_ptr: png); |
197 | updateRequired = true; |
198 | } |
199 | if ((colorType == PNG_COLOR_TYPE_GRAY) && (bitDepth < 8)) { |
200 | png_set_expand_gray_1_2_4_to_8(png_ptr: png); |
201 | updateRequired = true; |
202 | } |
203 | if (png_get_valid(png_ptr: png, info_ptr: info, PNG_INFO_tRNS)) { |
204 | png_set_tRNS_to_alpha(png_ptr: png); |
205 | updateRequired = true; |
206 | } |
207 | if (bitDepth < 8) { |
208 | png_set_packing(png_ptr: png); |
209 | updateRequired = true; |
210 | } |
211 | if (updateRequired) { |
212 | png_read_update_info(png_ptr: png, info_ptr: info); |
213 | } |
214 | } |
215 | |
216 | public: |
217 | PngEmbedder() = delete; |
218 | PngEmbedder(const PngEmbedder &) = delete; |
219 | PngEmbedder &operator=(const PngEmbedder &) = delete; |
220 | ~PngEmbedder() override |
221 | { |
222 | png_destroy_read_struct(png_ptr_ptr: &m_png, info_ptr_ptr: &m_info, end_info_ptr_ptr: nullptr); |
223 | delete m_stream; |
224 | } |
225 | |
226 | Ref embedImage(XRef *xref) override; |
227 | |
228 | static std::unique_ptr<ImageEmbedder> create(std::unique_ptr<uint8_t[]> &&fileContent, const Goffset fileSize) |
229 | { |
230 | png_structp png = png_create_read_struct(PNG_LIBPNG_VER_STRING, error_ptr: nullptr, error_fn: nullptr, warn_fn: nullptr); |
231 | if (png == nullptr) { |
232 | error(category: errInternal, pos: -1, msg: "Couldn't load PNG. png_create_read_struct() failed" ); |
233 | return nullptr; |
234 | } |
235 | png_infop info = png_create_info_struct(png_ptr: png); |
236 | if (info == nullptr) { |
237 | error(category: errInternal, pos: -1, msg: "Couldn't load PNG. png_create_info_struct() failed" ); |
238 | png_destroy_read_struct(png_ptr_ptr: &png, info_ptr_ptr: nullptr, end_info_ptr_ptr: nullptr); |
239 | return nullptr; |
240 | } |
241 | if (setjmp(png_jmpbuf(png))) { |
242 | error(category: errInternal, pos: -1, msg: "Couldn't load PNG. Failed to set up error handling for reading PNG" ); |
243 | png_destroy_read_struct(png_ptr_ptr: &png, info_ptr_ptr: &info, end_info_ptr_ptr: nullptr); |
244 | return nullptr; |
245 | } |
246 | |
247 | LibpngInputStream *stream = new LibpngInputStream(std::move(fileContent), fileSize); |
248 | png_set_read_fn(png_ptr: png, io_ptr: stream, read_data_fn: LibpngInputStream::readCallback); |
249 | png_read_info(png_ptr: png, info_ptr: info); |
250 | fixPng(png, info); |
251 | const png_byte bitDepth = png_get_bit_depth(png_ptr: png, info_ptr: info); |
252 | if ((bitDepth != 8) && (bitDepth != 16)) { |
253 | error(category: errInternal, pos: -1, msg: "Couldn't load PNG. Fixing bit depth failed" ); |
254 | png_destroy_read_struct(png_ptr_ptr: &png, info_ptr_ptr: &info, end_info_ptr_ptr: nullptr); |
255 | delete stream; |
256 | return nullptr; |
257 | } |
258 | return std::unique_ptr<ImageEmbedder>(new PngEmbedder(png, info, stream)); |
259 | } |
260 | }; |
261 | |
262 | Ref PngEmbedder::embedImage(XRef *xref) |
263 | { |
264 | // Read pixels. |
265 | Goffset area; |
266 | if (checkedMultiply(x: static_cast<Goffset>(m_width), y: static_cast<Goffset>(m_height), z: &area)) { |
267 | error(category: errIO, pos: -1, msg: "PngEmbedder::embedImage: width * height overflows Goffset" ); |
268 | return Ref::INVALID(); |
269 | } |
270 | Goffset maskBufferSize; |
271 | static_assert(sizeof(Goffset) >= sizeof(m_byteDepth)); |
272 | if (checkedMultiply(x: area, y: static_cast<Goffset>(m_byteDepth), z: &maskBufferSize)) { |
273 | error(category: errIO, pos: -1, msg: "PngEmbedder::embedImage: width * height * m_byteDepth overflows Goffset" ); |
274 | return Ref::INVALID(); |
275 | } |
276 | Goffset mainBufferSize; |
277 | static_assert(sizeof(Goffset) >= sizeof(m_nWithoutAlpha)); |
278 | if (checkedMultiply(x: maskBufferSize, y: static_cast<Goffset>(m_nWithoutAlpha), z: &mainBufferSize)) { |
279 | error(category: errIO, pos: -1, msg: "PngEmbedder::embedImage: width * height * m_byteDepth * m_nWithoutAlpha overflows Goffset" ); |
280 | return Ref::INVALID(); |
281 | } |
282 | png_bytep mainBuffer = (png_bytep)gmalloc(size: mainBufferSize); |
283 | png_bytep maskBuffer = (m_hasAlpha) ? (png_bytep)gmalloc(size: maskBufferSize) : nullptr; |
284 | readPixels(mainBuffer, maskBuffer); |
285 | |
286 | // Create a mask XObject and a main XObject. |
287 | const char *colorSpace = ((m_type == PNG_COLOR_TYPE_GRAY) || (m_type == PNG_COLOR_TYPE_GRAY_ALPHA)) ? DEVICE_GRAY : DEVICE_RGB; |
288 | Dict *baseImageDict = createImageDict(xref, colorSpace, width: m_width, height: m_height, bitsPerComponent: m_bitDepth); |
289 | if (m_hasAlpha) { |
290 | Dict *maskImageDict = createImageDict(xref, colorSpace: DEVICE_GRAY, width: m_width, height: m_height, bitsPerComponent: m_bitDepth); |
291 | Ref maskImageRef = xref->addStreamObject(dict: maskImageDict, buffer: maskBuffer, bufferSize: maskBufferSize, compression: StreamCompression::Compress); |
292 | baseImageDict->add(key: "SMask" , val: Object(maskImageRef)); |
293 | } |
294 | return xref->addStreamObject(dict: baseImageDict, buffer: mainBuffer, bufferSize: mainBufferSize, compression: StreamCompression::Compress); |
295 | } |
296 | #endif |
297 | |
298 | #ifdef ENABLE_LIBJPEG |
299 | |
300 | struct JpegErrorManager |
301 | { |
302 | jpeg_error_mgr pub; |
303 | jmp_buf setjmpBuffer; |
304 | }; |
305 | |
306 | // Note: an address of pub is equal to an address of a JpegErrorManager instance. |
307 | static void jpegExitErrorHandler(j_common_ptr info) |
308 | { |
309 | JpegErrorManager *errorManager = (JpegErrorManager *)info->err; |
310 | (*errorManager->pub.output_message)(info); |
311 | // Jump to the setjmp point. |
312 | longjmp(env: errorManager->setjmpBuffer, val: 1); |
313 | } |
314 | |
315 | // Transforms a JPEG image to XObject. |
316 | class JpegEmbedder : public ImageEmbedder |
317 | { |
318 | std::unique_ptr<uint8_t[]> m_fileContent; |
319 | Goffset m_fileSize; |
320 | |
321 | JpegEmbedder(const int width, const int height, std::unique_ptr<uint8_t[]> &&fileContent, const Goffset fileSize) : ImageEmbedder(width, height), m_fileContent(std::move(fileContent)), m_fileSize(fileSize) { } |
322 | |
323 | public: |
324 | JpegEmbedder() = delete; |
325 | JpegEmbedder(const JpegEmbedder &) = delete; |
326 | JpegEmbedder &operator=(const JpegEmbedder &) = delete; |
327 | ~JpegEmbedder() override = default; |
328 | |
329 | Ref embedImage(XRef *xref) override; |
330 | |
331 | static std::unique_ptr<ImageEmbedder> create(std::unique_ptr<uint8_t[]> &&fileContent, const Goffset fileSize) |
332 | { |
333 | jpeg_decompress_struct info; |
334 | JpegErrorManager errorManager; |
335 | info.err = jpeg_std_error(err: &errorManager.pub); |
336 | errorManager.pub.error_exit = jpegExitErrorHandler; |
337 | if (setjmp(errorManager.setjmpBuffer)) { |
338 | // The setjmp point. |
339 | jpeg_destroy_decompress(cinfo: &info); |
340 | error(category: errInternal, pos: -1, msg: "libjpeg failed to process the file" ); |
341 | return nullptr; |
342 | } |
343 | |
344 | jpeg_create_decompress(&info); |
345 | // fileSize is guaranteed to be in the range 0..int max by the checks in embed() |
346 | // jpeg_mem_src takes an unsigned long in the 3rd parameter |
347 | jpeg_mem_src(cinfo: &info, inbuffer: fileContent.get(), insize: static_cast<unsigned long>(fileSize)); |
348 | jpeg_read_header(cinfo: &info, TRUE); |
349 | jpeg_start_decompress(cinfo: &info); |
350 | auto result = std::unique_ptr<ImageEmbedder>(new JpegEmbedder(info.output_width, info.output_height, std::move(fileContent), fileSize)); |
351 | jpeg_abort_decompress(cinfo: &info); |
352 | jpeg_destroy_decompress(cinfo: &info); |
353 | return result; |
354 | } |
355 | }; |
356 | |
357 | Ref JpegEmbedder::embedImage(XRef *xref) |
358 | { |
359 | if (m_fileContent == nullptr) { |
360 | return Ref::INVALID(); |
361 | } |
362 | Dict *baseImageDict = createImageDict(xref, colorSpace: DEVICE_RGB, width: m_width, height: m_height, bitsPerComponent: 8); |
363 | baseImageDict->add(key: "Filter" , val: Object(objName, "DCTDecode" )); |
364 | Ref baseImageRef = xref->addStreamObject(dict: baseImageDict, buffer: m_fileContent.release(), bufferSize: m_fileSize, compression: StreamCompression::None); |
365 | return baseImageRef; |
366 | } |
367 | #endif |
368 | |
369 | Ref embed(XRef *xref, const GooFile &imageFile) |
370 | { |
371 | // Load the image file. |
372 | const Goffset fileSize = imageFile.size(); |
373 | if (fileSize < 0) { |
374 | error(category: errIO, pos: -1, msg: "Image file size could not be calculated" ); |
375 | return Ref::INVALID(); |
376 | } |
377 | // GooFile::read only takes an integer so for now we don't support huge images |
378 | if (fileSize > std::numeric_limits<int>::max()) { |
379 | error(category: errIO, pos: -1, msg: "file size too big" ); |
380 | return Ref::INVALID(); |
381 | } |
382 | std::unique_ptr<uint8_t[]> fileContent = std::make_unique<uint8_t[]>(num: fileSize); |
383 | const int bytesRead = imageFile.read(buf: (char *)fileContent.get(), n: static_cast<int>(fileSize), offset: 0); |
384 | if ((bytesRead != fileSize) || (fileSize < MAX_MAGIC_NUM_SIZE)) { |
385 | error(category: errIO, pos: -1, msg: "Couldn't load the image file" ); |
386 | return Ref::INVALID(); |
387 | } |
388 | |
389 | std::unique_ptr<ImageEmbedder> embedder; |
390 | if (checkMagicNum(fileContent: fileContent.get(), magicNum: PNG_MAGIC_NUM, size: sizeof(PNG_MAGIC_NUM))) { |
391 | #ifdef ENABLE_LIBPNG |
392 | embedder = PngEmbedder::create(fileContent: std::move(fileContent), fileSize); |
393 | #else |
394 | error(errUnimplemented, -1, "PNG format is not supported" ); |
395 | #endif |
396 | } else if (checkMagicNum(fileContent: fileContent.get(), magicNum: JPEG_MAGIC_NUM, size: sizeof(JPEG_MAGIC_NUM))) { |
397 | #ifdef ENABLE_LIBJPEG |
398 | embedder = JpegEmbedder::create(fileContent: std::move(fileContent), fileSize); |
399 | #else |
400 | error(errUnimplemented, -1, "JPEG format is not supported" ); |
401 | #endif |
402 | } else if (checkMagicNum(fileContent: fileContent.get(), magicNum: JPEG2000_MAGIC_NUM, size: sizeof(JPEG2000_MAGIC_NUM))) { |
403 | // TODO: implement JPEG2000 support using libopenjpeg2. |
404 | error(category: errUnimplemented, pos: -1, msg: "JPEG2000 format is not supported" ); |
405 | return Ref::INVALID(); |
406 | } else { |
407 | error(category: errUnimplemented, pos: -1, msg: "Image format is not supported" ); |
408 | return Ref::INVALID(); |
409 | } |
410 | |
411 | if (!embedder) { |
412 | return Ref::INVALID(); |
413 | } |
414 | return embedder->embedImage(xref); |
415 | } |
416 | |
417 | Ref embed(XRef *xref, const std::string &imagePath) |
418 | { |
419 | std::unique_ptr<GooFile> imageFile(GooFile::open(fileName: imagePath)); |
420 | if (!imageFile) { |
421 | error(category: errIO, pos: -1, msg: "Couldn't open {0:s}" , imagePath.c_str()); |
422 | return Ref::INVALID(); |
423 | } |
424 | return embed(xref, imageFile: *imageFile); |
425 | } |
426 | |
427 | } |
428 | |