File Coverage

src/pdfmake_attach.c
Criterion Covered Total %
statement 117 184 63.5
branch 51 160 31.8
condition n/a
subroutine n/a
pod n/a
total 168 344 48.8


line stmt bran cond sub pod time code
1             /*
2             * pdfmake_attach.c — File attachments and embedded files.
3             *
4             * §7.11.3 File Specification Dictionaries
5             * §7.11.4 Embedded File Streams
6             * §14.13 Embedded Files
7             */
8              
9             #include "pdfmake_attach.h"
10             #include "pdfmake_arena.h"
11             #include "pdfmake_filter.h"
12             #include
13             #include
14             #include
15              
16             /* ── MIME type guessing from filename extension ─────────── */
17              
18 1           static const char *_guess_mime(const char *filename) {
19 1           const char *dot = strrchr(filename, '.');
20 1 50         if (!dot) return "application/octet-stream";
21 1           dot++;
22 1 50         if (strcasecmp(dot, "pdf") == 0) return "application/pdf";
23 1 50         if (strcasecmp(dot, "txt") == 0) return "text/plain";
24 1 50         if (strcasecmp(dot, "html") == 0) return "text/html";
25 1 50         if (strcasecmp(dot, "htm") == 0) return "text/html";
26 1 50         if (strcasecmp(dot, "xml") == 0) return "application/xml";
27 1 50         if (strcasecmp(dot, "json") == 0) return "application/json";
28 0 0         if (strcasecmp(dot, "csv") == 0) return "text/csv";
29 0 0         if (strcasecmp(dot, "jpg") == 0) return "image/jpeg";
30 0 0         if (strcasecmp(dot, "jpeg") == 0) return "image/jpeg";
31 0 0         if (strcasecmp(dot, "png") == 0) return "image/png";
32 0 0         if (strcasecmp(dot, "gif") == 0) return "image/gif";
33 0 0         if (strcasecmp(dot, "svg") == 0) return "image/svg+xml";
34 0 0         if (strcasecmp(dot, "zip") == 0) return "application/zip";
35 0 0         if (strcasecmp(dot, "xlsx") == 0) return "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet";
36 0 0         if (strcasecmp(dot, "docx") == 0) return "application/vnd.openxmlformats-officedocument.wordprocessingml.document";
37 0           return "application/octet-stream";
38             }
39              
40             /* ── Create ────────────────────────────────────────────── */
41              
42 25           pdfmake_attachment_t *pdfmake_doc_attach(
43             pdfmake_doc_t *doc,
44             const char *name,
45             const char *filename,
46             const uint8_t *data, size_t len,
47             const char *mime_type,
48             const char *description)
49             {
50             size_t new_cap;
51             void **new_arr;
52             pdfmake_attachment_t *att;
53              
54 25 50         if (!doc || !name || !data) return NULL;
    50          
    50          
55              
56 25 100         if (doc->attach_count >= doc->attach_cap) {
57 10 100         new_cap = doc->attach_cap == 0 ? 4 : doc->attach_cap * 2;
58 10           new_arr = realloc(doc->attachments, new_cap * sizeof(void *));
59 10 50         if (!new_arr) return NULL;
60 10           doc->attachments = new_arr;
61 10           doc->attach_cap = new_cap;
62             }
63              
64 25           att = calloc(1, sizeof(pdfmake_attachment_t));
65 25 50         if (!att) return NULL;
66              
67 25           strncpy(att->name, name, sizeof(att->name) - 1);
68 25 100         strncpy(att->filename, filename ? filename : name, sizeof(att->filename) - 1);
69              
70 25 100         if (mime_type) {
71 24           strncpy(att->mime_type, mime_type, sizeof(att->mime_type) - 1);
72             } else {
73 1           strncpy(att->mime_type, _guess_mime(att->filename), sizeof(att->mime_type) - 1);
74             }
75              
76 25 100         if (description) {
77 3           strncpy(att->description, description, sizeof(att->description) - 1);
78             }
79              
80 25           att->data = malloc(len);
81 25 50         if (!att->data) { free(att); return NULL; }
82 25           memcpy(att->data, data, len);
83 25           att->data_len = len;
84              
85 25           doc->attachments[doc->attach_count++] = att;
86 25           return att;
87             }
88              
89 0           pdfmake_attachment_t *pdfmake_doc_attach_file(
90             pdfmake_doc_t *doc,
91             const char *name,
92             const char *path)
93             {
94             FILE *fp;
95             long file_len;
96             uint8_t *buf;
97             size_t nread;
98             const char *slash;
99             const char *bslash;
100             const char *fname;
101             pdfmake_attachment_t *att;
102              
103 0 0         if (!doc || !name || !path) return NULL;
    0          
    0          
104              
105 0           fp = fopen(path, "rb");
106 0 0         if (!fp) return NULL;
107              
108 0           fseek(fp, 0, SEEK_END);
109 0           file_len = ftell(fp);
110 0 0         if (file_len < 0) { fclose(fp); return NULL; }
111 0           rewind(fp);
112              
113 0           buf = malloc((size_t)file_len);
114 0 0         if (!buf) { fclose(fp); return NULL; }
115              
116 0           nread = fread(buf, 1, (size_t)file_len, fp);
117 0           fclose(fp);
118              
119 0 0         if ((long)nread != file_len) { free(buf); return NULL; }
120              
121             /* Extract filename from path */
122 0           slash = strrchr(path, '/');
123 0           bslash = strrchr(path, '\\');
124 0           fname = path;
125 0 0         if (slash && slash > fname) fname = slash + 1;
    0          
126 0 0         if (bslash && bslash > fname) fname = bslash + 1;
    0          
127              
128 0           att = pdfmake_doc_attach(doc, name, fname,
129             buf, (size_t)file_len, NULL, NULL);
130 0           free(buf);
131 0           return att;
132             }
133              
134             /* ── Query ─────────────────────────────────────────────── */
135              
136 0           size_t pdfmake_doc_attachment_count(pdfmake_doc_t *doc) {
137 0 0         return doc ? doc->attach_count : 0;
138             }
139              
140 0           pdfmake_attachment_t *pdfmake_doc_attachment_at(pdfmake_doc_t *doc, size_t idx) {
141 0 0         if (!doc || idx >= doc->attach_count) return NULL;
    0          
142 0           return (pdfmake_attachment_t *)doc->attachments[idx];
143             }
144              
145 0           pdfmake_attachment_t *pdfmake_doc_attachment_by_name(pdfmake_doc_t *doc, const char *name) {
146             size_t i;
147             pdfmake_attachment_t *att;
148              
149 0 0         if (!doc || !name) return NULL;
    0          
150 0 0         for (i = 0; i < doc->attach_count; i++) {
151 0           att = (pdfmake_attachment_t *)doc->attachments[i];
152 0 0         if (strcmp(att->name, name) == 0) return att;
153             }
154 0           return NULL;
155             }
156              
157             /* ── Properties ────────────────────────────────────────── */
158              
159 0           const char *pdfmake_attachment_name(pdfmake_attachment_t *att) {
160 0 0         return att ? att->name : NULL;
161             }
162 0           const char *pdfmake_attachment_filename(pdfmake_attachment_t *att) {
163 0 0         return att ? att->filename : NULL;
164             }
165 0           const char *pdfmake_attachment_mime_type(pdfmake_attachment_t *att) {
166 0 0         return att ? att->mime_type : NULL;
167             }
168 0           size_t pdfmake_attachment_size(pdfmake_attachment_t *att) {
169 0 0         return att ? att->data_len : 0;
170             }
171              
172             /* ── Extract ───────────────────────────────────────────── */
173              
174 0           const uint8_t *pdfmake_attachment_data(pdfmake_attachment_t *att, size_t *out_len) {
175 0 0         if (!att) { if (out_len) *out_len = 0; return NULL; }
    0          
176 0 0         if (out_len) *out_len = att->data_len;
177 0           return att->data;
178             }
179              
180 0           pdfmake_err_t pdfmake_attachment_extract_to_file(pdfmake_attachment_t *att, const char *path) {
181             FILE *fp;
182             size_t written;
183              
184 0 0         if (!att || !path || !att->data) return PDFMAKE_EINVAL;
    0          
    0          
185 0           fp = fopen(path, "wb");
186 0 0         if (!fp) return PDFMAKE_EINVAL;
187 0           written = fwrite(att->data, 1, att->data_len, fp);
188 0           fclose(fp);
189 0 0         return (written == att->data_len) ? PDFMAKE_OK : PDFMAKE_EINVAL;
190             }
191              
192             /* ── Write ─────────────────────────────────────────────── */
193              
194 4           uint32_t pdfmake_attachment_write(pdfmake_attachment_t *att, pdfmake_doc_t *doc) {
195             pdfmake_arena_t *arena;
196             uint32_t k;
197             pdfmake_buf_t compressed;
198             pdfmake_flate_params_t params;
199             int use_flate;
200             pdfmake_obj_t ef_stream;
201             pdfmake_obj_t ef_dict_obj;
202             char encoded[256];
203             size_t ei;
204             size_t i;
205             pdfmake_obj_t ef_params;
206             pdfmake_obj_t fs;
207             pdfmake_obj_t ef_dict;
208             pdfmake_obj_t ef_ref;
209              
210 4 50         if (!att || !doc || !att->data) return 0;
    50          
    50          
211 4 50         if (att->fs_obj_num) return att->fs_obj_num;
212              
213 4           arena = pdfmake_doc_arena(doc);
214              
215             /* Compress data with FlateDecode */
216 4           pdfmake_buf_init(&compressed);
217 4           memset(¶ms, 0, sizeof(params));
218 4           params.predictor = 1;
219 4           use_flate = (pdfmake_flate_encode(att->data, att->data_len, ¶ms, &compressed) == PDFMAKE_OK);
220              
221             /* Create embedded file stream */
222 4           ef_stream = pdfmake_stream_new(arena);
223 4 50         if (ef_stream.kind != PDFMAKE_STREAM) {
224 0           pdfmake_buf_free(&compressed);
225 0           return 0;
226             }
227              
228 4 50         if (use_flate && compressed.len < att->data_len) {
    50          
229 0           pdfmake_stream_set_data(arena, &ef_stream, compressed.data, compressed.len);
230             } else {
231 4           pdfmake_stream_set_data(arena, &ef_stream, att->data, att->data_len);
232 4           use_flate = 0;
233             }
234              
235             /* Set stream dict entries */
236 4           ef_dict_obj.kind = PDFMAKE_DICT;
237 4           ef_dict_obj.as.dict = pdfmake_stream_dict(&ef_stream);
238              
239 4           k = pdfmake_arena_intern_name(arena, "Type", 4);
240 4           pdfmake_dict_set(arena, &ef_dict_obj, k, pdfmake_name_cstr(arena, "EmbeddedFile"));
241              
242             /* MIME subtype: encode / as #2F per PDF spec */
243 4 50         if (att->mime_type[0]) {
244 4           ei = 0;
245 50 100         for (i = 0; att->mime_type[i] && ei < sizeof(encoded) - 4; i++) {
    50          
246 46 100         if (att->mime_type[i] == '/') {
247 4           encoded[ei++] = '#'; encoded[ei++] = '2'; encoded[ei++] = 'F';
248             } else {
249 42           encoded[ei++] = att->mime_type[i];
250             }
251             }
252 4           encoded[ei] = '\0';
253 4           k = pdfmake_arena_intern_name(arena, "Subtype", 7);
254 4           pdfmake_dict_set(arena, &ef_dict_obj, k, pdfmake_name(arena, encoded, ei));
255             }
256              
257 4 50         if (use_flate) {
258 0           k = pdfmake_arena_intern_name(arena, "Filter", 6);
259 0           pdfmake_dict_set(arena, &ef_dict_obj, k, pdfmake_name_cstr(arena, "FlateDecode"));
260             }
261              
262             /* Params */
263 4           ef_params = pdfmake_dict_new(arena);
264 4           k = pdfmake_arena_intern_name(arena, "Size", 4);
265 4           pdfmake_dict_set(arena, &ef_params, k, pdfmake_int((int64_t)att->data_len));
266 4           k = pdfmake_arena_intern_name(arena, "Params", 6);
267 4           pdfmake_dict_set(arena, &ef_dict_obj, k, ef_params);
268              
269 4           k = pdfmake_arena_intern_name(arena, "Length", 6);
270 8 50         pdfmake_dict_set(arena, &ef_dict_obj, k,
271 4           pdfmake_int((int64_t)(use_flate ? compressed.len : att->data_len)));
272              
273 4           att->ef_obj_num = pdfmake_doc_add(doc, ef_stream);
274 4           pdfmake_buf_free(&compressed);
275 4 50         if (att->ef_obj_num == 0) return 0;
276              
277             /* Create Filespec dictionary */
278 4           fs = pdfmake_dict_new(arena);
279 4           k = pdfmake_arena_intern_name(arena, "Type", 4);
280 4           pdfmake_dict_set(arena, &fs, k, pdfmake_name_cstr(arena, "Filespec"));
281              
282 4           k = pdfmake_arena_intern_name(arena, "F", 1);
283 4           pdfmake_dict_set(arena, &fs, k, pdfmake_str_cstr(arena, att->filename));
284              
285 4           k = pdfmake_arena_intern_name(arena, "UF", 2);
286 4           pdfmake_dict_set(arena, &fs, k, pdfmake_str_cstr(arena, att->filename));
287              
288 4 100         if (att->description[0]) {
289 3           k = pdfmake_arena_intern_name(arena, "Desc", 4);
290 3           pdfmake_dict_set(arena, &fs, k, pdfmake_str_cstr(arena, att->description));
291             }
292              
293             /* /EF << /F ref /UF ref >> */
294 4           ef_dict = pdfmake_dict_new(arena);
295 4           ef_ref = pdfmake_ref(att->ef_obj_num, 0);
296 4           k = pdfmake_arena_intern_name(arena, "F", 1);
297 4           pdfmake_dict_set(arena, &ef_dict, k, ef_ref);
298 4           k = pdfmake_arena_intern_name(arena, "UF", 2);
299 4           pdfmake_dict_set(arena, &ef_dict, k, ef_ref);
300 4           k = pdfmake_arena_intern_name(arena, "EF", 2);
301 4           pdfmake_dict_set(arena, &fs, k, ef_dict);
302              
303             /* AFRelationship */
304 4           k = pdfmake_arena_intern_name(arena, "AFRelationship", 14);
305 4           pdfmake_dict_set(arena, &fs, k, pdfmake_name_cstr(arena, "Data"));
306              
307 4           att->fs_obj_num = pdfmake_doc_add(doc, fs);
308 4           return att->fs_obj_num;
309             }
310              
311             /* ── Write /Names/EmbeddedFiles into catalog ───────────── */
312              
313 3           pdfmake_err_t pdfmake_doc_write_attachments(pdfmake_doc_t *doc) {
314             pdfmake_arena_t *arena;
315             uint32_t k;
316             size_t i;
317             pdfmake_attachment_t *att;
318             pdfmake_obj_t names_arr;
319             pdfmake_obj_t ef_tree;
320             pdfmake_obj_t names_dict;
321             pdfmake_obj_t *catalog;
322              
323 3 50         if (!doc || doc->attach_count == 0) return PDFMAKE_OK;
    50          
324              
325 3           arena = pdfmake_doc_arena(doc);
326              
327             /* Ensure all attachments are written */
328 7 100         for (i = 0; i < doc->attach_count; i++) {
329 4           att = (pdfmake_attachment_t *)doc->attachments[i];
330 4 50         if (!att->fs_obj_num) {
331 4 50         if (pdfmake_attachment_write(att, doc) == 0)
332 0           return PDFMAKE_ENOMEM;
333             }
334             }
335              
336             /* Build /Names array: [(name1) ref1 (name2) ref2 ...] */
337 3           names_arr = pdfmake_array_new(arena);
338 7 100         for (i = 0; i < doc->attach_count; i++) {
339 4           att = (pdfmake_attachment_t *)doc->attachments[i];
340 4           pdfmake_array_push(arena, &names_arr,
341 4           pdfmake_str_cstr(arena, att->name));
342 4           pdfmake_array_push(arena, &names_arr,
343             pdfmake_ref(att->fs_obj_num, 0));
344             }
345              
346             /* /EmbeddedFiles << /Names [...] >> */
347 3           ef_tree = pdfmake_dict_new(arena);
348 3           k = pdfmake_arena_intern_name(arena, "Names", 5);
349 3           pdfmake_dict_set(arena, &ef_tree, k, names_arr);
350              
351             /* /Names << /EmbeddedFiles ... >> */
352 3           names_dict = pdfmake_dict_new(arena);
353 3           k = pdfmake_arena_intern_name(arena, "EmbeddedFiles", 13);
354 3           pdfmake_dict_set(arena, &names_dict, k, ef_tree);
355              
356             /* Add to catalog */
357 3           catalog = pdfmake_doc_get(doc, doc->root_num);
358 3 50         if (!catalog || catalog->kind != PDFMAKE_DICT) return PDFMAKE_EINVAL;
    50          
359              
360 3           k = pdfmake_arena_intern_name(arena, "Names", 5);
361 3           pdfmake_dict_set(arena, catalog, k, names_dict);
362              
363 3           return PDFMAKE_OK;
364             }