File Coverage

src/pdfmake_image.c
Criterion Covered Total %
statement 204 359 56.8
branch 72 176 40.9
condition n/a
subroutine n/a
pod n/a
total 276 535 51.5


line stmt bran cond sub pod time code
1             /*
2             * pdfmake_image.c — Image embedding: JPEG passthrough, PNG decode+re-encode, raw raster.
3             *
4             * §8.9 Images, §7.4.8 DCTDecode, §7.4.4 FlateDecode + predictor
5             */
6              
7             #include "pdfmake_image.h"
8             #include "pdfmake_page.h"
9             #include "pdfmake_arena.h"
10             #include "pdfmake_buf.h"
11             #include "pdfmake_filter.h"
12             #include "pdfmake_writer.h"
13             #include "pdfmake_internal.h"
14             #include
15             #include
16              
17             /* ── JPEG header parser ──────────────────────────────────────────────────── */
18              
19             /* JPEG markers */
20             #define JPEG_SOI 0xFFD8
21             #define JPEG_SOF0 0xFFC0 /* Baseline DCT */
22             #define JPEG_SOF1 0xFFC1 /* Extended sequential DCT */
23             #define JPEG_SOF2 0xFFC2 /* Progressive DCT */
24             #define JPEG_EOI 0xFFD9
25              
26 4           static int jpeg_parse_header(const uint8_t *data, size_t len,
27             uint32_t *width, uint32_t *height,
28             uint8_t *components, uint8_t *bpc)
29             {
30             size_t pos;
31             uint8_t marker;
32             uint16_t seg_len;
33 4 50         if (len < 2 || data[0] != 0xFF || data[1] != 0xD8)
    50          
    50          
34 0           return 0; /* Not a JPEG */
35              
36 4           pos = 2;
37 12 50         while (pos + 4 <= len) {
38 12 50         if (data[pos] != 0xFF) return 0;
39              
40 12           marker = data[pos + 1];
41 12           pos += 2;
42              
43             /* Markers without payload */
44 12 50         if (marker == 0x00 || marker == 0x01 || (marker >= 0xD0 && marker <= 0xD7))
    50          
    100          
    50          
45 0           continue;
46              
47 12 50         if (marker == 0xD9) break; /* EOI */
48              
49 12 50         if (pos + 2 > len) return 0;
50 12           seg_len = ((uint16_t)data[pos] << 8) | data[pos + 1];
51 12 50         if (seg_len < 2) return 0;
52              
53             /* SOF0, SOF1, SOF2 — Start of Frame */
54 12 100         if (marker == 0xC0 || marker == 0xC1 || marker == 0xC2) {
    50          
    50          
55 4 50         if (pos + seg_len > len || seg_len < 8) return 0;
    50          
56 4           *bpc = data[pos + 2];
57 4           *height = ((uint32_t)data[pos + 3] << 8) | data[pos + 4];
58 4           *width = ((uint32_t)data[pos + 5] << 8) | data[pos + 6];
59 4           *components = data[pos + 7];
60 4           return 1;
61             }
62              
63 8           pos += seg_len;
64             }
65 0           return 0; /* No SOF found */
66             }
67              
68 4           pdfmake_image_t *pdfmake_image_from_jpeg(pdfmake_doc_t *doc,
69             const uint8_t *bytes, size_t len)
70             {
71             uint32_t w, h;
72             uint8_t comp, bpc;
73             pdfmake_image_t *img;
74             (void)doc; /* doc not needed for JPEG passthrough */
75 4 50         if (!bytes || len < 4) return NULL;
    50          
76              
77 4 50         if (!jpeg_parse_header(bytes, len, &w, &h, &comp, &bpc))
78 0           return NULL;
79              
80 4           img = calloc(1, sizeof(pdfmake_image_t));
81 4 50         if (!img) return NULL;
82              
83 4           img->format = PDFMAKE_IMAGE_JPEG;
84 4           img->width = w;
85 4           img->height = h;
86 4           img->bits_per_component = bpc;
87 4           img->components = comp;
88 4           img->has_alpha = 0;
89              
90 4           switch (comp) {
91 0           case 1: img->colorspace = PDFMAKE_IMAGE_GRAY; break;
92 4           case 3: img->colorspace = PDFMAKE_IMAGE_RGB; break;
93 0           case 4: img->colorspace = PDFMAKE_IMAGE_CMYK; break;
94 0           default: free(img); return NULL;
95             }
96              
97             /* DCTDecode passthrough: store the raw JPEG bytes */
98 4           img->data = malloc(len);
99 4 50         if (!img->data) { free(img); return NULL; }
100 4           memcpy(img->data, bytes, len);
101 4           img->data_len = len;
102              
103 4           return img;
104             }
105              
106             /* ── PNG parser ──────────────────────────────────────────────────────────── */
107              
108             /* PNG magic: 137 80 78 71 13 10 26 10 */
109             static const uint8_t PNG_SIG[8] = {137, 80, 78, 71, 13, 10, 26, 10};
110              
111 15           pdfmake_image_t *pdfmake_image_from_png(pdfmake_doc_t *doc,
112             const uint8_t *bytes, size_t len)
113             {
114             size_t pos;
115             uint32_t ihdr_len;
116             uint32_t w;
117             uint32_t h;
118             uint8_t bit_depth;
119             uint8_t color_type;
120             uint8_t interlace;
121             uint8_t components;
122             int has_alpha;
123             pdfmake_image_colorspace_t cs;
124             pdfmake_buf_t idat_buf;
125             uint8_t *palette;
126             size_t palette_len;
127             uint8_t *trns;
128             size_t trns_len;
129             uint32_t chunk_len;
130             const uint8_t *chunk_type;
131             const uint8_t *chunk_data;
132             pdfmake_buf_t raw_buf;
133             pdfmake_flate_params_t params;
134             pdfmake_buf_t unfiltered;
135             uint8_t *color_data;
136             size_t color_len;
137             uint8_t *alpha_data;
138             size_t alpha_len;
139             uint8_t color_comp;
140             size_t row_color_bytes;
141             size_t row_alpha_bytes;
142             size_t src_row;
143             uint32_t row;
144             const uint8_t *src;
145             uint8_t *cdst;
146             uint8_t *adst;
147             uint32_t col;
148             size_t bpp;
149             pdfmake_buf_t encoded;
150             pdfmake_flate_params_t enc_params;
151             uint8_t *smask_enc;
152             size_t smask_enc_len;
153             pdfmake_buf_t smask_buf;
154             pdfmake_flate_params_t smask_params;
155             pdfmake_image_t *img;
156             (void)doc; /* doc not needed during parse — we use malloc */
157 15 50         if (!bytes || len < 33) return NULL;
    50          
158 15 50         if (memcmp(bytes, PNG_SIG, 8) != 0) return NULL;
159              
160             /* Parse IHDR (must be first chunk after signature) */
161 15           pos = 8;
162 15           ihdr_len = pdfmake_read_be32(bytes + pos);
163 15 50         if (memcmp(bytes + pos + 4, "IHDR", 4) != 0 || ihdr_len != 13)
    50          
164 0           return NULL;
165 15           pos += 8; /* skip length + "IHDR" */
166              
167 15           w = pdfmake_read_be32(bytes + pos);
168 15           h = pdfmake_read_be32(bytes + pos + 4);
169 15           bit_depth = bytes[pos + 8];
170 15           color_type = bytes[pos + 9];
171 15           interlace = bytes[pos + 12];
172              
173 15 50         if (interlace != 0) return NULL; /* Adam7 not supported */
174 15 50         if (bit_depth != 8 && bit_depth != 16) return NULL; /* Simplification */
    0          
175              
176 15           pos += ihdr_len + 4; /* skip IHDR data + CRC */
177              
178             /* Determine components and colorspace */
179 15           has_alpha = 0;
180              
181 15           switch (color_type) {
182 0           case 0: components = 1; cs = PDFMAKE_IMAGE_GRAY; break; /* Grayscale */
183 0           case 2: components = 3; cs = PDFMAKE_IMAGE_RGB; break; /* RGB */
184 15           case 3: components = 1; cs = PDFMAKE_IMAGE_INDEXED; break; /* Indexed */
185 0           case 4: components = 2; cs = PDFMAKE_IMAGE_GRAY; has_alpha = 1; break; /* Gray+Alpha */
186 0           case 6: components = 4; cs = PDFMAKE_IMAGE_RGB; has_alpha = 1; break; /* RGBA */
187 0           default: return NULL;
188             }
189              
190             /* Collect PLTE, tRNS, and all IDAT chunks */
191 15           pdfmake_buf_init(&idat_buf);
192              
193 15           palette = NULL;
194 15           palette_len = 0;
195 15           trns = NULL;
196 15           trns_len = 0;
197              
198 90 50         while (pos + 12 <= len) {
199 90           chunk_len = pdfmake_read_be32(bytes + pos);
200 90           chunk_type = bytes + pos + 4;
201 90           chunk_data = bytes + pos + 8;
202              
203 90 50         if (pos + 12 + chunk_len > len) break;
204              
205 90 100         if (memcmp(chunk_type, "IDAT", 4) == 0) {
206 60           pdfmake_buf_append(&idat_buf, chunk_data, chunk_len);
207             }
208 30 100         else if (memcmp(chunk_type, "PLTE", 4) == 0) {
209 15           palette = malloc(chunk_len);
210 15 50         if (palette) {
211 15           memcpy(palette, chunk_data, chunk_len);
212 15           palette_len = chunk_len;
213             }
214             }
215 15 50         else if (memcmp(chunk_type, "tRNS", 4) == 0) {
216 0           trns = malloc(chunk_len);
217 0 0         if (trns) {
218 0           memcpy(trns, chunk_data, chunk_len);
219 0           trns_len = chunk_len;
220             }
221             }
222 15 50         else if (memcmp(chunk_type, "IEND", 4) == 0) {
223 15           break;
224             }
225              
226 75           pos += 12 + chunk_len; /* length(4) + type(4) + data + crc(4) */
227             }
228              
229 15 50         if (idat_buf.len == 0) {
230 0           pdfmake_buf_free(&idat_buf);
231 0           free(palette); free(trns);
232 0           return NULL;
233             }
234              
235             /* Decompress IDAT (zlib-wrapped deflate) */
236 15           pdfmake_buf_init(&raw_buf);
237              
238 15           memset(¶ms, 0, sizeof(params));
239 15           params.predictor = 1; /* No predictor for initial inflate */
240 15 50         if (pdfmake_flate_decode(idat_buf.data, idat_buf.len, ¶ms, &raw_buf) != PDFMAKE_OK) {
241 0           pdfmake_buf_free(&idat_buf);
242 0           pdfmake_buf_free(&raw_buf);
243 0           free(palette); free(trns);
244 0           return NULL;
245             }
246 15           pdfmake_buf_free(&idat_buf);
247              
248             /* Reverse PNG row filters */
249 15           pdfmake_buf_init(&unfiltered);
250              
251 15 50         if (pdfmake_predictor_decode(15, components, bit_depth, w,
252 15           raw_buf.data, raw_buf.len, &unfiltered) != PDFMAKE_OK) {
253 0           pdfmake_buf_free(&raw_buf);
254 0           pdfmake_buf_free(&unfiltered);
255 0           free(palette); free(trns);
256 0           return NULL;
257             }
258 15           pdfmake_buf_free(&raw_buf);
259              
260             /* Now unfiltered has raw pixel data (w * components * bpc/8) * h bytes */
261              
262             /* Separate alpha channel if present */
263 15           color_data = NULL;
264 15           color_len = 0;
265 15           alpha_data = NULL;
266 15           alpha_len = 0;
267              
268 15 50         if (has_alpha) {
269 0           color_comp = components - 1; /* RGB=3, Gray=1 */
270 0           row_color_bytes = (size_t)w * color_comp * (bit_depth / 8);
271 0           row_alpha_bytes = (size_t)w * (bit_depth / 8);
272 0           color_len = row_color_bytes * h;
273 0           alpha_len = row_alpha_bytes * h;
274              
275 0           color_data = malloc(color_len);
276 0           alpha_data = malloc(alpha_len);
277 0 0         if (!color_data || !alpha_data) {
    0          
278 0           free(color_data); free(alpha_data);
279 0           pdfmake_buf_free(&unfiltered);
280 0           free(palette); free(trns);
281 0           return NULL;
282             }
283              
284 0           src_row = (size_t)w * components * (bit_depth / 8);
285 0 0         for (row = 0; row < h; row++) {
286 0           src = unfiltered.data + row * src_row;
287 0           cdst = color_data + row * row_color_bytes;
288 0           adst = alpha_data + row * row_alpha_bytes;
289 0 0         for (col = 0; col < w; col++) {
290 0           bpp = bit_depth / 8;
291 0           memcpy(cdst, src, color_comp * bpp);
292 0           memcpy(adst, src + color_comp * bpp, bpp);
293 0           src += components * bpp;
294 0           cdst += color_comp * bpp;
295 0           adst += bpp;
296             }
297             }
298 0           components = color_comp;
299             } else {
300 15           color_len = unfiltered.len;
301 15           color_data = malloc(color_len);
302 15 50         if (!color_data) {
303 0           pdfmake_buf_free(&unfiltered);
304 0           free(palette); free(trns);
305 0           return NULL;
306             }
307 15           memcpy(color_data, unfiltered.data, color_len);
308             }
309 15           pdfmake_buf_free(&unfiltered);
310              
311             /* Re-encode color data with FlateDecode + PNG predictor for PDF */
312 15           pdfmake_buf_init(&encoded);
313 15           memset(&enc_params, 0, sizeof(enc_params));
314 15           enc_params.predictor = 15; /* PNG Optimum */
315 15 50         enc_params.colors = (cs == PDFMAKE_IMAGE_INDEXED) ? 1 : components;
316 15           enc_params.bits_per_comp = bit_depth;
317 15           enc_params.columns = w;
318              
319 15 50         if (pdfmake_flate_encode(color_data, color_len, &enc_params, &encoded) != PDFMAKE_OK) {
320 0           free(color_data); free(alpha_data);
321 0           pdfmake_buf_free(&encoded);
322 0           free(palette); free(trns);
323 0           return NULL;
324             }
325 15           free(color_data);
326              
327             /* Re-encode alpha (SMask) */
328 15           smask_enc = NULL;
329 15           smask_enc_len = 0;
330 15 50         if (alpha_data) {
331 0           pdfmake_buf_init(&smask_buf);
332 0           memset(&smask_params, 0, sizeof(smask_params));
333 0           smask_params.predictor = 15;
334 0           smask_params.colors = 1;
335 0           smask_params.bits_per_comp = bit_depth;
336 0           smask_params.columns = w;
337              
338 0 0         if (pdfmake_flate_encode(alpha_data, alpha_len, &smask_params, &smask_buf) == PDFMAKE_OK) {
339 0           smask_enc = smask_buf.data;
340 0           smask_enc_len = smask_buf.len;
341             /* Don't free smask_buf — we're stealing .data */
342             }
343 0           free(alpha_data);
344             }
345              
346             /* Build image struct */
347 15           img = calloc(1, sizeof(pdfmake_image_t));
348 15 50         if (!img) {
349 0           pdfmake_buf_free(&encoded);
350 0           free(smask_enc); free(palette); free(trns);
351 0           return NULL;
352             }
353              
354 15           img->format = PDFMAKE_IMAGE_PNG;
355 15           img->colorspace = cs;
356 15           img->width = w;
357 15           img->height = h;
358 15           img->bits_per_component = bit_depth;
359 15           img->components = components;
360 15           img->has_alpha = has_alpha;
361              
362 15           img->data = encoded.data;
363 15           img->data_len = encoded.len;
364             /* Don't free encoded — we're stealing .data */
365              
366 15           img->smask_data = smask_enc;
367 15           img->smask_len = smask_enc_len;
368              
369 15           img->palette = palette;
370 15           img->palette_len = palette_len;
371 15           img->trns = trns;
372 15           img->trns_len = trns_len;
373              
374 15           return img;
375             }
376              
377             /* ── Raw raster ──────────────────────────────────────────────────────────── */
378              
379 0           pdfmake_image_t *pdfmake_image_from_raw(pdfmake_doc_t *doc,
380             const pdfmake_image_raw_t *raw)
381             {
382             pdfmake_image_t *img;
383             pdfmake_buf_t encoded;
384             pdfmake_flate_params_t params;
385             pdfmake_buf_t smask_buf;
386 0 0         if (!doc || !raw || !raw->pixels) return NULL;
    0          
    0          
387              
388 0           img = calloc(1, sizeof(pdfmake_image_t));
389 0 0         if (!img) return NULL;
390              
391 0           img->format = PDFMAKE_IMAGE_RAW;
392 0           img->colorspace = raw->colorspace;
393 0           img->width = raw->width;
394 0           img->height = raw->height;
395 0           img->bits_per_component = raw->bits_per_component;
396              
397 0           switch (raw->colorspace) {
398 0           case PDFMAKE_IMAGE_GRAY: img->components = 1; break;
399 0           case PDFMAKE_IMAGE_RGB: img->components = 3; break;
400 0           case PDFMAKE_IMAGE_CMYK: img->components = 4; break;
401 0           case PDFMAKE_IMAGE_INDEXED: img->components = 1; break;
402             }
403              
404             /* Compress with FlateDecode */
405 0           pdfmake_buf_init(&encoded);
406 0           memset(¶ms, 0, sizeof(params));
407 0           params.predictor = 1; /* No predictor for raw */
408              
409 0 0         if (pdfmake_flate_encode(raw->pixels, raw->pixels_len, ¶ms, &encoded) != PDFMAKE_OK) {
410 0           free(img);
411 0           pdfmake_buf_free(&encoded);
412 0           return NULL;
413             }
414              
415 0           img->data = encoded.data;
416 0           img->data_len = encoded.len;
417              
418             /* Alpha channel */
419 0 0         if (raw->alpha && raw->alpha_len > 0) {
    0          
420 0           img->has_alpha = 1;
421 0           pdfmake_buf_init(&smask_buf);
422 0 0         if (pdfmake_flate_encode(raw->alpha, raw->alpha_len, ¶ms, &smask_buf) == PDFMAKE_OK) {
423 0           img->smask_data = smask_buf.data;
424 0           img->smask_len = smask_buf.len;
425             }
426             }
427              
428             /* Palette */
429 0 0         if (raw->palette && raw->palette_len > 0) {
    0          
430 0           img->palette = malloc(raw->palette_len);
431 0 0         if (img->palette) {
432 0           memcpy(img->palette, raw->palette, raw->palette_len);
433 0           img->palette_len = raw->palette_len;
434             }
435             }
436              
437 0           return img;
438             }
439              
440             /* ── Auto-detect ─────────────────────────────────────────────────────────── */
441              
442 19           pdfmake_image_t *pdfmake_image_from_bytes(pdfmake_doc_t *doc,
443             const uint8_t *bytes, size_t len)
444             {
445 19 50         if (!bytes || len < 8) return NULL;
    50          
446              
447             /* JPEG: starts with FF D8 */
448 19 100         if (bytes[0] == 0xFF && bytes[1] == 0xD8)
    50          
449 4           return pdfmake_image_from_jpeg(doc, bytes, len);
450              
451             /* PNG: starts with 89 50 4E 47 */
452 15 50         if (memcmp(bytes, PNG_SIG, 8) == 0)
453 15           return pdfmake_image_from_png(doc, bytes, len);
454              
455 0           return NULL; /* Unknown format */
456             }
457              
458             /* ── Write /Image XObject ────────────────────────────────────────────────── */
459              
460 17           uint32_t pdfmake_image_write(pdfmake_image_t *img, pdfmake_doc_t *doc)
461             {
462             pdfmake_arena_t *arena;
463             pdfmake_obj_t smask_stream;
464             pdfmake_obj_t smask_dict_obj;
465             pdfmake_obj_t *smask_dict;
466             uint32_t k;
467             pdfmake_obj_t dp;
468             uint32_t pred_k;
469             uint32_t cols_k;
470             uint32_t bpc_k;
471             uint32_t col_k;
472             pdfmake_obj_t img_stream;
473             pdfmake_obj_t img_dict_obj;
474             pdfmake_obj_t *img_dict;
475             pdfmake_obj_t cs_arr;
476 17 50         if (!img || !doc || !img->data) return 0;
    50          
    50          
477              
478 17           arena = pdfmake_doc_arena(doc);
479              
480             /* Write SMask first if present */
481 17 50         if (img->has_alpha && img->smask_data) {
    0          
482 0           smask_stream = pdfmake_stream_new(arena);
483 0 0         if (smask_stream.kind != PDFMAKE_STREAM) return 0;
484              
485 0           pdfmake_stream_set_data(arena, &smask_stream, img->smask_data, img->smask_len);
486             /* smask_data is already Flate-encoded in pdfmake_image_from_png/raw */
487 0 0         if (smask_stream.as.stream) {
488 0           smask_stream.as.stream->filtered = 1;
489             }
490              
491             /* Wrap stream's dict in an obj for pdfmake_dict_set */
492 0           smask_dict_obj.kind = PDFMAKE_DICT;
493 0           smask_dict_obj.as.dict = pdfmake_stream_dict(&smask_stream);
494 0           smask_dict = &smask_dict_obj;
495              
496 0           k = pdfmake_arena_intern_name(arena, "Type", 4);
497 0           pdfmake_dict_set(arena, smask_dict, k, pdfmake_name_cstr(arena, "XObject"));
498 0           k = pdfmake_arena_intern_name(arena, "Subtype", 7);
499 0           pdfmake_dict_set(arena, smask_dict, k, pdfmake_name_cstr(arena, "Image"));
500 0           k = pdfmake_arena_intern_name(arena, "Width", 5);
501 0           pdfmake_dict_set(arena, smask_dict, k, pdfmake_int(img->width));
502 0           k = pdfmake_arena_intern_name(arena, "Height", 6);
503 0           pdfmake_dict_set(arena, smask_dict, k, pdfmake_int(img->height));
504 0           k = pdfmake_arena_intern_name(arena, "ColorSpace", 10);
505 0           pdfmake_dict_set(arena, smask_dict, k, pdfmake_name_cstr(arena, "DeviceGray"));
506 0           k = pdfmake_arena_intern_name(arena, "BitsPerComponent", 16);
507 0           pdfmake_dict_set(arena, smask_dict, k, pdfmake_int(img->bits_per_component));
508 0           k = pdfmake_arena_intern_name(arena, "Filter", 6);
509 0           pdfmake_dict_set(arena, smask_dict, k, pdfmake_name_cstr(arena, "FlateDecode"));
510              
511             /* DecodeParms for PNG predictor */
512 0 0         if (img->format == PDFMAKE_IMAGE_PNG) {
513 0           dp = pdfmake_dict_new(arena);
514 0           pred_k = pdfmake_arena_intern_name(arena, "Predictor", 9);
515 0           cols_k = pdfmake_arena_intern_name(arena, "Columns", 7);
516 0           bpc_k = pdfmake_arena_intern_name(arena, "BitsPerComponent", 16);
517 0           col_k = pdfmake_arena_intern_name(arena, "Colors", 6);
518 0           pdfmake_dict_set(arena, &dp, pred_k, pdfmake_int(15));
519 0           pdfmake_dict_set(arena, &dp, cols_k, pdfmake_int(img->width));
520 0           pdfmake_dict_set(arena, &dp, bpc_k, pdfmake_int(img->bits_per_component));
521 0           pdfmake_dict_set(arena, &dp, col_k, pdfmake_int(1));
522 0           k = pdfmake_arena_intern_name(arena, "DecodeParms", 11);
523 0           pdfmake_dict_set(arena, smask_dict, k, dp);
524             }
525              
526 0           k = pdfmake_arena_intern_name(arena, "Length", 6);
527 0           pdfmake_dict_set(arena, smask_dict, k, pdfmake_int((int64_t)img->smask_len));
528              
529 0           img->smask_num = pdfmake_doc_add(doc, smask_stream);
530 0 0         if (img->smask_num == 0) return 0;
531             }
532              
533             /* Build /Image XObject stream */
534 17           img_stream = pdfmake_stream_new(arena);
535 17 50         if (img_stream.kind != PDFMAKE_STREAM) return 0;
536              
537 17           pdfmake_stream_set_data(arena, &img_stream, img->data, img->data_len);
538             /* PNG/Raw image data is already Flate-encoded before write */
539 17 50         if (img_stream.as.stream && img->format != PDFMAKE_IMAGE_JPEG) {
    100          
540 15           img_stream.as.stream->filtered = 1;
541             }
542              
543             /* Wrap stream's dict in an obj for pdfmake_dict_set */
544 17           img_dict_obj.kind = PDFMAKE_DICT;
545 17           img_dict_obj.as.dict = pdfmake_stream_dict(&img_stream);
546 17           img_dict = &img_dict_obj;
547              
548 17           k = pdfmake_arena_intern_name(arena, "Type", 4);
549 17           pdfmake_dict_set(arena, img_dict, k, pdfmake_name_cstr(arena, "XObject"));
550 17           k = pdfmake_arena_intern_name(arena, "Subtype", 7);
551 17           pdfmake_dict_set(arena, img_dict, k, pdfmake_name_cstr(arena, "Image"));
552 17           k = pdfmake_arena_intern_name(arena, "Width", 5);
553 17           pdfmake_dict_set(arena, img_dict, k, pdfmake_int(img->width));
554 17           k = pdfmake_arena_intern_name(arena, "Height", 6);
555 17           pdfmake_dict_set(arena, img_dict, k, pdfmake_int(img->height));
556 17           k = pdfmake_arena_intern_name(arena, "BitsPerComponent", 16);
557 17           pdfmake_dict_set(arena, img_dict, k, pdfmake_int(img->bits_per_component));
558              
559             /* ColorSpace */
560 17           k = pdfmake_arena_intern_name(arena, "ColorSpace", 10);
561 17           switch (img->colorspace) {
562 0           case PDFMAKE_IMAGE_GRAY:
563 0           pdfmake_dict_set(arena, img_dict, k, pdfmake_name_cstr(arena, "DeviceGray"));
564 0           break;
565 2           case PDFMAKE_IMAGE_RGB:
566 2           pdfmake_dict_set(arena, img_dict, k, pdfmake_name_cstr(arena, "DeviceRGB"));
567 2           break;
568 0           case PDFMAKE_IMAGE_CMYK:
569 0           pdfmake_dict_set(arena, img_dict, k, pdfmake_name_cstr(arena, "DeviceCMYK"));
570 0           break;
571 15           case PDFMAKE_IMAGE_INDEXED: {
572             /* [/Indexed /DeviceRGB hival ] */
573 15           cs_arr = pdfmake_array_new(arena);
574 15           pdfmake_array_push(arena, &cs_arr, pdfmake_name_cstr(arena, "Indexed"));
575 15           pdfmake_array_push(arena, &cs_arr, pdfmake_name_cstr(arena, "DeviceRGB"));
576 15           pdfmake_array_push(arena, &cs_arr, pdfmake_int((int64_t)(img->palette_len / 3 - 1)));
577 15           pdfmake_array_push(arena, &cs_arr,
578 15           pdfmake_hexstr(arena, img->palette, img->palette_len));
579 15           pdfmake_dict_set(arena, img_dict, k, cs_arr);
580 15           break;
581             }
582             }
583              
584             /* Filter */
585 17           k = pdfmake_arena_intern_name(arena, "Filter", 6);
586 17 100         if (img->format == PDFMAKE_IMAGE_JPEG) {
587 2           pdfmake_dict_set(arena, img_dict, k, pdfmake_name_cstr(arena, "DCTDecode"));
588             } else {
589 15           pdfmake_dict_set(arena, img_dict, k, pdfmake_name_cstr(arena, "FlateDecode"));
590              
591             /* DecodeParms for PNG predictor */
592 15 50         if (img->format == PDFMAKE_IMAGE_PNG) {
593 15           dp = pdfmake_dict_new(arena);
594 15           pred_k = pdfmake_arena_intern_name(arena, "Predictor", 9);
595 15           cols_k = pdfmake_arena_intern_name(arena, "Columns", 7);
596 15           bpc_k = pdfmake_arena_intern_name(arena, "BitsPerComponent", 16);
597 15           col_k = pdfmake_arena_intern_name(arena, "Colors", 6);
598 15           pdfmake_dict_set(arena, &dp, pred_k, pdfmake_int(15));
599 15           pdfmake_dict_set(arena, &dp, cols_k, pdfmake_int(img->width));
600 15           pdfmake_dict_set(arena, &dp, bpc_k, pdfmake_int(img->bits_per_component));
601 15           pdfmake_dict_set(arena, &dp, col_k, pdfmake_int(img->components));
602 15           k = pdfmake_arena_intern_name(arena, "DecodeParms", 11);
603 15           pdfmake_dict_set(arena, img_dict, k, dp);
604             }
605             }
606              
607             /* SMask reference */
608 17 50         if (img->smask_num) {
609 0           k = pdfmake_arena_intern_name(arena, "SMask", 5);
610 0           pdfmake_dict_set(arena, img_dict, k, pdfmake_ref(img->smask_num, 0));
611             }
612              
613             /* Length */
614 17           k = pdfmake_arena_intern_name(arena, "Length", 6);
615 17           pdfmake_dict_set(arena, img_dict, k, pdfmake_int((int64_t)img->data_len));
616              
617 17           img->obj_num = pdfmake_doc_add(doc, img_stream);
618 17           return img->obj_num;
619             }
620              
621             /* ── Cleanup ─────────────────────────────────────────────────────────────── */
622              
623 19           void pdfmake_image_free(pdfmake_image_t *img)
624             {
625 19 50         if (!img) return;
626 19           free(img->data);
627 19           free(img->smask_data);
628 19           free(img->palette);
629 19           free(img->trns);
630 19           free(img);
631             }
632              
633             /* ── Page XObject resource ───────────────────────────────────────────────── */
634              
635 17           int pdfmake_page_add_image(pdfmake_page_t *page, const char *name, uint32_t img_obj_num)
636             {
637             pdfmake_image_entry_t *entry;
638 17 50         if (!page || !name || img_obj_num == 0) return -1;
    50          
    50          
639 17 50         if (page->image_count >= PDFMAKE_MAX_PAGE_IMAGES) return -1;
640              
641 17           entry = &page->images[page->image_count++];
642 17           strncpy(entry->name, name, sizeof(entry->name) - 1);
643 17           entry->name[sizeof(entry->name) - 1] = '\0';
644 17           entry->image_num = img_obj_num;
645              
646 17           return (int)(page->image_count - 1);
647             }