Coverage Report

Created: 2026-08-14 07:34

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
/src/mupdf/source/html/office.c
Line
Count
Source
1
// Copyright (C) 2023-2026 Artifex Software, Inc.
2
//
3
// This file is part of MuPDF.
4
//
5
// MuPDF is free software: you can redistribute it and/or modify it under the
6
// terms of the GNU Affero General Public License as published by the Free
7
// Software Foundation, either version 3 of the License, or (at your option)
8
// any later version.
9
//
10
// MuPDF is distributed in the hope that it will be useful, but WITHOUT ANY
11
// WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
12
// FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
13
// details.
14
//
15
// You should have received a copy of the GNU Affero General Public License
16
// along with MuPDF. If not, see <https://www.gnu.org/licenses/agpl-3.0.en.html>
17
//
18
// Alternative licensing terms are available from the licensor.
19
// For commercial licensing, see <https://www.artifex.com/> or contact
20
// Artifex Software, Inc., 39 Mesa Street, Suite 108A, San Francisco,
21
// CA 94129, USA, for further information.
22
23
#include "mupdf/fitz.h"
24
#include "html-imp.h"
25
26
#include <limits.h>
27
28
//#define DEBUG_OFFICE_TO_HTML
29
30
/* Defaults are all 0's. FIXME: Very subject to change. Possibly might be removed entirely. */
31
typedef struct
32
{
33
  int output_page_numbers;
34
  int output_sheet_names;
35
  int output_cell_markers;
36
  int output_cell_row_markers;
37
  int output_cell_names;
38
  int output_formatting;
39
  int output_filenames;
40
  int output_errors;
41
}
42
fz_office_to_html_opts;
43
44
typedef struct
45
{
46
  fz_office_to_html_opts opts;
47
48
  fz_output *out;
49
50
  int page;
51
52
  /* State for if we are parsing a sheet. */
53
  /* The last column label we have to send. */
54
  char *label;
55
  /* Columns are numbered from 1. */
56
  /* The column we are at. */
57
  int col_at;
58
  /* The column we last signalled. If this is 0, then we haven't
59
   * even started a row yet. */
60
  int col_signalled;
61
62
  /* If we are currently processing a spreadsheet, store the current
63
   * sheets name here. */
64
  const char *sheet_name;
65
66
  fz_list(char *, shared_strings);
67
68
  fz_list(char *, footnotes);
69
70
  char *title;
71
} doc_info;
72
73
static void
74
doc_escape(fz_context *ctx, fz_output *output, const char *str_)
75
0
{
76
0
  const unsigned char *str = (const unsigned char *)str_;
77
0
  int c;
78
79
0
  if (!str)
80
0
    return;
81
82
0
  while ((c = *str++) != 0)
83
0
  {
84
0
    if (c == '&')
85
0
    {
86
0
      fz_write_string(ctx, output, "&amp;");
87
0
    }
88
0
    else if (c == '<')
89
0
    {
90
0
      fz_write_string(ctx, output, "&lt;");
91
0
    }
92
0
    else if (c == '>')
93
0
    {
94
0
      fz_write_string(ctx, output, "&gt;");
95
0
    }
96
0
    else
97
0
    {
98
      /* We get utf-8 in, just parrot it out again. */
99
0
      fz_write_byte(ctx, output, c);
100
0
    }
101
0
  }
102
0
}
103
104
static void
105
show_text(fz_context *ctx, fz_xml *top, doc_info *info)
106
0
{
107
0
  fz_xml *pos = top;
108
0
  fz_xml *next;
109
110
0
  while (pos)
111
0
  {
112
0
    doc_escape(ctx, info->out, fz_xml_text(pos));
113
114
0
    if (fz_xml_is_tag(pos, "lineBreak"))
115
0
    {
116
0
      fz_write_string(ctx, info->out, "\n");
117
0
    }
118
0
    else if (fz_xml_is_tag(pos, "tab"))
119
0
    {
120
0
      fz_write_string(ctx, info->out, "\t");
121
0
    }
122
0
    else if (fz_xml_is_tag(pos, "lastRenderedPageBreak"))
123
0
    {
124
0
      info->page++;
125
0
    }
126
127
    /* Always try to move down. */
128
0
    next = fz_xml_down(pos);
129
0
    if (next)
130
0
    {
131
      /* We can move down, easy! */
132
0
      pos = next;
133
0
      continue;
134
0
    }
135
136
0
    if (pos == top)
137
0
      break;
138
139
    /* We can't move down, try moving to next. */
140
0
    next = fz_xml_next(pos);
141
0
    if (next)
142
0
    {
143
      /* We can move to next, easy! */
144
0
      pos = next;
145
0
      continue;
146
0
    }
147
148
    /* If we can't go down, or next, pop up until we
149
     * find somewhere we can go next from. */
150
0
    while (1)
151
0
    {
152
      /* OK. So move up. */
153
0
      pos = fz_xml_up(pos);
154
      /* Check for hitting the top. */
155
0
      if (pos == top)
156
0
        pos = NULL;
157
0
      if (pos == NULL)
158
0
        break;
159
      /* We've returned to a node. See if it's a 'p'. */
160
0
      if (fz_xml_is_tag(pos, "p"))
161
0
      {
162
0
        fz_write_string(ctx, info->out, "\n");
163
0
      }
164
0
      next = fz_xml_next(pos);
165
0
      if (next)
166
0
      {
167
0
        pos = next;
168
0
        break;
169
0
      }
170
0
    }
171
0
  }
172
0
}
173
174
static void
175
show_footnote(fz_context *ctx, fz_xml *v, doc_info *info)
176
0
{
177
0
  int n = fz_atoi(fz_xml_att(v, "w:id"));
178
179
0
  if (n < 0 || n >= info->footnotes_len)
180
0
    return;
181
182
0
  if (info->footnotes[n] == NULL ||
183
0
    info->footnotes[n][0] == 0)
184
0
    return;
185
186
  /* Then send the strings. */
187
0
  doc_escape(ctx, info->out, info->footnotes[n]);
188
0
}
189
190
static void
191
process_doc_stream(fz_context *ctx, fz_xml *xml, doc_info *info, int do_pages)
192
0
{
193
0
  fz_xml *pos;
194
0
  fz_xml *next;
195
0
  const char *paragraph_style = NULL;
196
0
  const char *inline_style = NULL;
197
198
#ifdef DEBUG_OFFICE_TO_HTML
199
  fz_write_printf(ctx, fz_stddbg(ctx), "process_doc_stream:\n");
200
  fz_output_xml(ctx, fz_stddbg(ctx), xml, 0);
201
#endif
202
203
  /* First off, see if we can do page numbers. */
204
0
  if (do_pages)
205
0
  {
206
0
    pos = fz_xml_find_dfs(xml, "lastRenderedPageBreak", NULL, NULL);
207
0
    if (pos)
208
0
    {
209
      /* We *can* do page numbers, so start here. */
210
0
      fz_write_string(ctx, info->out, "<div id=\"page1\">\n");
211
0
      info->page = 1;
212
0
    }
213
0
  }
214
215
  /* Now walk the tree for real. */
216
0
  pos = xml;
217
0
  while (pos)
218
0
  {
219
    /* When we arrive on a node, check if it's a 't'. */
220
0
    if (fz_xml_is_tag(pos, "t"))
221
0
    {
222
0
      show_text(ctx, pos, info);
223
      /* Do NOT go down, we've already dealt with that. */
224
0
    }
225
0
    else if (fz_xml_is_tag(pos, "br"))
226
0
    {
227
0
      if (paragraph_style && strcmp(paragraph_style, "pre"))
228
0
      {
229
0
        fz_write_printf(ctx, info->out, "<br/>\n");
230
0
      }
231
0
      else
232
0
      {
233
0
        fz_write_printf(ctx, info->out, "\n");
234
0
      }
235
0
    }
236
0
    else if (fz_xml_is_tag(pos, "footnoteReference"))
237
0
    {
238
0
      show_footnote(ctx, pos, info);
239
      /* Do NOT go down, we've already dealt with that. */
240
0
    }
241
0
    else if (fz_xml_is_tag(pos, "tabs"))
242
0
    {
243
      /* Don't walk through tabs, or we will hit lots of 'tab' entries and
244
       * output incorrect information. */
245
0
    }
246
0
    else if (fz_xml_is_tag(pos, "pStyle"))
247
0
    {
248
      /* Should prob fix fz_xml_*() to strip namespace prefix
249
      from attributes, to match what it does for tag names.
250
      */
251
0
      paragraph_style = fz_xml_att(pos, "w:val");
252
0
      if (paragraph_style)
253
0
      {
254
0
        if (!strcmp(paragraph_style, "BodyText"))
255
0
          paragraph_style = NULL;
256
0
        else if (!strcmp(paragraph_style, "Heading1"))
257
0
          paragraph_style = "h1";
258
0
        else if (!strcmp(paragraph_style, "Heading2"))
259
0
          paragraph_style = "h2";
260
0
        else if (!strcmp(paragraph_style, "Heading3"))
261
0
          paragraph_style = "h3";
262
0
        else if (!strcmp(paragraph_style, "Heading4"))
263
0
          paragraph_style = "h4";
264
0
        else if (!strcmp(paragraph_style, "Heading5"))
265
0
          paragraph_style = "h5";
266
0
        else if (!strcmp(paragraph_style, "Heading6"))
267
0
          paragraph_style = "h6";
268
0
        else if (!strcmp(paragraph_style, "SourceCode"))
269
0
          paragraph_style = "pre";
270
0
        else
271
0
          paragraph_style = NULL;
272
273
0
        if (paragraph_style)
274
0
          fz_write_printf(ctx, info->out, "<%s>", paragraph_style);
275
0
      }
276
0
    }
277
0
    else if (fz_xml_is_tag(pos, "rStyle"))
278
0
    {
279
0
      inline_style = fz_xml_att(pos, "w:val");
280
0
      if (inline_style)
281
0
      {
282
0
        if (!strcmp(inline_style, "VerbatimChar"))
283
0
          inline_style = "tt";
284
0
        else
285
0
        {
286
0
          if (0)
287
0
            fz_write_printf(ctx, info->out, "<!-- %s -->", inline_style);
288
0
          inline_style = NULL;
289
0
        }
290
0
        if (inline_style)
291
0
          fz_write_printf(ctx, info->out, "<%s>", inline_style);
292
0
      }
293
0
    }
294
0
    else
295
0
    {
296
0
      fz_xml *down;
297
0
      if (fz_xml_is_tag(pos, "lineBreak"))
298
0
      {
299
0
        fz_write_string(ctx, info->out, "\n");
300
0
      }
301
0
      else if (fz_xml_is_tag(pos, "p"))
302
0
      {
303
0
        fz_write_string(ctx, info->out, "<p>");
304
0
      }
305
0
      else if (fz_xml_is_tag(pos, "tab"))
306
0
      {
307
0
        fz_write_string(ctx, info->out, "\t");
308
0
      }
309
0
      else if (do_pages && fz_xml_is_tag(pos, "lastRenderedPageBreak"))
310
0
      {
311
0
        if (info->page)
312
0
          fz_write_string(ctx, info->out, "\n</div>\n");
313
0
        info->page++;
314
0
        fz_write_printf(ctx, info->out, "<div id=\"page%d\">\n", info->page);
315
0
      }
316
      /* Try to move down. */
317
0
      down = fz_xml_down(pos);
318
0
      if (down)
319
0
      {
320
        /* We can move down, easy! */
321
0
        pos = down;
322
0
        continue;
323
0
      }
324
0
    }
325
    /* Try moving to next. */
326
0
    next = fz_xml_next(pos);
327
0
    if (next)
328
0
    {
329
      /* We can move to next, easy! */
330
0
      pos = next;
331
0
      continue;
332
0
    }
333
334
    /* If we can't go down, or next, pop up until we
335
     * find somewhere we can go next from. */
336
0
    while (1)
337
0
    {
338
      /* OK. So move up. */
339
0
      pos = fz_xml_up(pos);
340
      /* Check for hitting the top. */
341
0
      if (pos == NULL)
342
0
        break;
343
      /* We've returned to a node. See if it's a 'p'. */
344
0
      if (fz_xml_is_tag(pos, "p"))
345
0
      {
346
0
        if (paragraph_style)
347
0
        {
348
0
          fz_write_printf(ctx, info->out, "</%s>", paragraph_style);
349
0
          paragraph_style = NULL;
350
0
        }
351
0
        fz_write_string(ctx, info->out, "</p>\n");
352
0
      }
353
0
      else if (fz_xml_is_tag(pos, "r"))
354
0
      {
355
        /* Seems to be pseudo-close for rStyle. */
356
0
        if (inline_style)
357
0
        {
358
0
          fz_write_printf(ctx, info->out, "</%s>", inline_style);
359
0
          inline_style = NULL;
360
0
        }
361
0
      }
362
0
      next = fz_xml_next(pos);
363
0
      if (next)
364
0
      {
365
0
        pos = next;
366
0
        break;
367
0
      }
368
0
    }
369
0
  }
370
371
0
  if (do_pages && info->page)
372
0
    fz_write_string(ctx, info->out, "\n</div>\n");
373
0
}
374
375
static void
376
process_item(fz_context *ctx, fz_archive *arch, const char *file, doc_info *info, int do_pages)
377
0
{
378
0
  fz_xml *xml = fz_parse_xml_archive_entry(ctx, arch, file, 1);
379
380
0
  fz_try(ctx)
381
0
    process_doc_stream(ctx, xml, info, do_pages);
382
0
  fz_always(ctx)
383
0
    fz_drop_xml(ctx, xml);
384
0
  fz_catch(ctx)
385
0
    fz_rethrow(ctx);
386
0
}
387
388
static void
389
process_rootfile(fz_context *ctx, fz_archive *arch, const char *file, doc_info *info)
390
0
{
391
0
  fz_xml *xml = fz_parse_xml_archive_entry(ctx, arch, file, 0);
392
393
0
  fz_try(ctx)
394
0
  {
395
    /* FIXME: Should really search for these just inside 'spine'. */
396
0
    fz_xml *pos = fz_xml_find_dfs(xml, "itemref", NULL, NULL);
397
0
    while (pos)
398
0
    {
399
0
      char *idref = fz_xml_att(pos, "idref");
400
0
      fz_xml *item = fz_xml_find_dfs(xml, "item", "id", idref);
401
0
      while (item)
402
0
      {
403
0
        char *type = fz_xml_att(item, "media-type");
404
0
        char *href = fz_xml_att(item, "href");
405
0
        if (type && href && !strcmp(type, "application/xml"))
406
0
        {
407
0
          process_item(ctx, arch, href, info, 1);
408
0
        }
409
0
        item = fz_xml_find_next_dfs(pos, "item", "id", idref);
410
0
      }
411
0
      pos = fz_xml_find_next_dfs(pos, "itemref", NULL, NULL);
412
0
    }
413
0
  }
414
0
  fz_always(ctx)
415
0
    fz_drop_xml(ctx, xml);
416
0
  fz_catch(ctx)
417
0
    fz_rethrow(ctx);
418
0
}
419
420
/* XLSX support */
421
static char *
422
make_rel_name(fz_context *ctx, const char *file)
423
0
{
424
0
  size_t z = strlen(file);
425
0
  char *s = fz_malloc(ctx, z + 12);
426
0
  char *t;
427
0
  const char *p;
428
0
  const char *slash = file;
429
430
0
  for (p = file; *p != 0; p++)
431
0
    if (*p == '/')
432
0
      slash = p+1;
433
434
0
  t = s;
435
0
  if (slash != file)
436
0
  {
437
0
    memcpy(t, file, slash - file);
438
0
    t += slash - file;
439
0
  }
440
0
  memcpy(t, "_rels/", 6);
441
0
  t += 6;
442
0
  memcpy(t, file + (slash - file), z - (slash - file));
443
0
  t += z - (slash - file);
444
0
  memcpy(t, ".rels", 6);
445
446
0
  return s;
447
0
}
448
449
static char *lookup_rel(fz_context *ctx, fz_xml *rels, const char *id)
450
0
{
451
0
  fz_xml *pos;
452
453
0
  if (id == NULL)
454
0
    return NULL;
455
456
0
  pos = fz_xml_find_dfs(rels, "Relationship", NULL, NULL);
457
0
  while (pos)
458
0
  {
459
0
    char *id2 = fz_xml_att(pos, "Id");
460
461
0
    if (id2 && !strcmp(id, id2))
462
0
      return fz_xml_att(pos, "Target");
463
464
0
    pos = fz_xml_find_next_dfs(pos, "Relationship", NULL, NULL);
465
0
  }
466
467
0
  return NULL;
468
0
}
469
470
static void
471
send_cell_formatting(fz_context *ctx, doc_info *info)
472
0
{
473
0
  if (info->col_signalled == 0)
474
0
  {
475
0
    fz_write_string(ctx, info->out, "<tr>\n");
476
0
    info->col_signalled = 1;
477
0
    if (info->col_at > 1)
478
0
      fz_write_string(ctx, info->out, "<td>");
479
0
  }
480
481
  /* Send the label */
482
0
  while (info->col_signalled < info->col_at)
483
0
  {
484
0
    fz_write_string(ctx, info->out, "</td>");
485
0
    info->col_signalled++;
486
0
    if (info->col_signalled < info->col_at)
487
0
      fz_write_string(ctx, info->out, "<td>");
488
0
  }
489
0
  if (info->sheet_name && info->sheet_name[0])
490
0
    fz_write_printf(ctx, info->out, "<td id=\"%s!%s\">", info->sheet_name, info->label);
491
0
  else
492
0
    fz_write_printf(ctx, info->out, "<td id=\"%s\">", info->label);
493
0
}
494
495
static void
496
show_shared_string(fz_context *ctx, fz_xml *v, doc_info *info)
497
0
{
498
0
  const char *t = fz_xml_text(fz_xml_down(v));
499
0
  int n = fz_atoi(t);
500
501
0
  if (n < 0 || n >= info->shared_strings_len)
502
0
    return;
503
504
0
  if (info->shared_strings[n] == NULL ||
505
0
    info->shared_strings[n][0] == 0)
506
0
    return;
507
508
0
  send_cell_formatting(ctx, info);
509
  /* Then send the strings. */
510
0
  doc_escape(ctx, info->out, info->shared_strings[n]);
511
0
}
512
513
static int
514
col_from_label(const char *label)
515
0
{
516
0
  int col = 0;
517
0
  int len = 26;
518
0
  int base = 0;
519
520
  /* If we can't read the column, return 0. */
521
0
  if (label == NULL || *label < 'A' || *label > 'Z')
522
0
    return 0;
523
524
  /*  Each section (A-Z, AA-ZZ, AAA-ZZZ etc) is of len 'len', and starts
525
   *  at base index 'base'. Each section is 26 times as long, and starts
526
   *  at base + len from the previous section.
527
   *
528
   *  A:  col = 26 * 0 + 0 + 0
529
   *  AA: col = (26 * 0 + 0 + 0) * 26 + 0 + 26 = 26
530
   *  AAA:  col = (((26 * 0 + 0 + 0) * 26 + 0 + 26)*26 + 0 + 26*26 = 26 + 26 * 26
531
   */
532
0
  do
533
0
  {
534
0
    col = 26 * col + (*label++) - 'A' + base;
535
0
    base += len;
536
0
    len *= 26;
537
0
  }
538
0
  while (*label >= 'A' && *label <= 'Z');
539
540
0
  return col+1;
541
0
}
542
543
static void
544
show_cell_text(fz_context *ctx, fz_xml *top, doc_info *info)
545
0
{
546
0
  fz_xml *pos = top;
547
0
  fz_xml *next;
548
549
0
  while (pos)
550
0
  {
551
0
    char *text = fz_xml_text(pos);
552
553
0
    if (text)
554
0
    {
555
0
      send_cell_formatting(ctx, info);
556
0
      doc_escape(ctx, info->out, text);
557
0
    }
558
559
    /* Always try to move down. */
560
0
    next = fz_xml_down(pos);
561
0
    if (next)
562
0
    {
563
      /* We can move down, easy! */
564
0
      pos = next;
565
0
      continue;
566
0
    }
567
568
0
    if (pos == top)
569
0
      break;
570
571
    /* We can't move down, try moving to next. */
572
0
    next = fz_xml_next(pos);
573
0
    if (next)
574
0
    {
575
      /* We can move to next, easy! */
576
0
      pos = next;
577
0
      continue;
578
0
    }
579
580
    /* If we can't go down, or next, pop up until we
581
     * find somewhere we can go next from. */
582
0
    while (1)
583
0
    {
584
      /* OK. So move up. */
585
0
      pos = fz_xml_up(pos);
586
      /* Check for hitting the top. */
587
0
      if (pos == top)
588
0
        pos = NULL;
589
0
      if (pos == NULL)
590
0
        break;
591
0
      next = fz_xml_next(pos);
592
0
      if (next)
593
0
      {
594
0
        pos = next;
595
0
        break;
596
0
      }
597
0
    }
598
0
  }
599
0
}
600
601
static void
602
arrived_at_cell(fz_context *ctx, doc_info *info, const char *label)
603
0
{
604
0
  int col;
605
606
  /* If we have a label queued, and no label is given here, then we're
607
   * processing a 'cell' callback after having had a 'cellname'
608
   * callback. So don't signal it twice! */
609
0
  if (label == NULL && info->label)
610
0
    return;
611
612
0
  col = label ? col_from_label(label) : 0;
613
614
0
  fz_free(ctx, info->label);
615
0
  info->label = NULL;
616
0
  info->label = label ? fz_strdup(ctx, label) : NULL;
617
0
  info->col_at = col;
618
0
}
619
620
static void
621
show_cell(fz_context *ctx, fz_xml *cell, doc_info *info)
622
0
{
623
0
  char *t = fz_xml_att(cell, "t");
624
0
  fz_xml *v = fz_xml_find_down(cell, "v");
625
0
  const char *r = fz_xml_att(cell, "r");
626
627
0
  arrived_at_cell(ctx, info, r);
628
0
  if (t && t[0] == 's' && t[1] == 0)
629
0
    show_shared_string(ctx, v, info);
630
0
  else
631
0
    show_cell_text(ctx, v, info);
632
0
}
633
634
static void
635
new_row(fz_context *ctx, doc_info *info)
636
0
{
637
0
  if (info->col_signalled)
638
0
  {
639
    /* We've sent at least one cell. So need to close the
640
     * td and tr */
641
0
    fz_write_string(ctx, info->out, "</td>\n</tr>\n");
642
0
  }
643
0
  else
644
0
  {
645
    /* We've not sent anything for this row. Keep the counts
646
     * correct. */
647
0
    fz_write_string(ctx, info->out, "<tr></tr>\n");
648
0
  }
649
0
  info->col_at = 1;
650
0
  info->col_signalled = 0;
651
0
  fz_free(ctx, info->label);
652
0
  info->label = NULL;
653
0
}
654
655
static void
656
process_sheet(fz_context *ctx, fz_archive *arch, const char *name, const char *file, doc_info *info)
657
0
{
658
0
  fz_xml *xml = fz_parse_xml_archive_entry(ctx, arch, file, 1);
659
660
#ifdef DEBUG_OFFICE_TO_HTML
661
  fz_write_printf(ctx, fz_stddbg(ctx), "process_sheet:\n");
662
  fz_output_xml(ctx, fz_stddbg(ctx), xml, 0);
663
#endif
664
665
0
  fz_write_printf(ctx, info->out, "<table id=\"%s\">\n", name);
666
667
0
  info->sheet_name = name;
668
0
  info->col_at = 0;
669
0
  info->col_signalled = 0;
670
671
0
  fz_try(ctx)
672
0
  {
673
0
    fz_xml *pos = xml;
674
0
    fz_xml *next;
675
676
0
    while (pos)
677
0
    {
678
      /* When we arrive on a node, check if it's a cell. */
679
0
      if (fz_xml_is_tag(pos, "c"))
680
0
      {
681
0
        show_cell(ctx, pos, info);
682
        /* Do NOT go down, we've already dealt with that. */
683
0
      }
684
0
      else
685
0
      {
686
        /* Try to move down. */
687
0
        next = fz_xml_down(pos);
688
0
        if (next)
689
0
        {
690
          /* We can move down, easy! */
691
0
          pos = next;
692
0
          continue;
693
0
        }
694
0
      }
695
      /* Try moving to next. */
696
0
      next = fz_xml_next(pos);
697
0
      if (next)
698
0
      {
699
        /* We can move to next, easy! */
700
0
        pos = next;
701
0
        continue;
702
0
      }
703
704
      /* If we can't go down, or next, pop up until we
705
       * find somewhere we can go next from. */
706
0
      while (1)
707
0
      {
708
        /* OK. So move up. */
709
0
        pos = fz_xml_up(pos);
710
        /* Check for hitting the top. */
711
0
        if (pos == NULL)
712
0
          break;
713
714
        /* We've returned to a node. See if it's a 'row'. */
715
0
        if (fz_xml_is_tag(pos, "row"))
716
0
          new_row(ctx, info);
717
718
0
        next = fz_xml_next(pos);
719
0
        if (next)
720
0
        {
721
0
          pos = next;
722
0
          break;
723
0
        }
724
0
      }
725
0
    }
726
0
    if (info->col_signalled)
727
0
      fz_write_printf(ctx, info->out, "</td>\n</tr>\n");
728
0
    fz_write_printf(ctx, info->out, "</table>\n");
729
0
  }
730
0
  fz_always(ctx)
731
0
    fz_drop_xml(ctx, xml);
732
0
  fz_catch(ctx)
733
0
    fz_rethrow(ctx);
734
0
}
735
736
static void
737
process_slide(fz_context *ctx, fz_archive *arch, const char *file, doc_info *info)
738
0
{
739
0
  fz_write_printf(ctx, info->out, "<div id=\"slide%d\">\n", info->page++);
740
0
  process_item(ctx, arch, file, info, 0);
741
0
  fz_write_printf(ctx, info->out, "</div>\n");
742
0
}
743
744
static char *
745
make_absolute_path(fz_context *ctx, const char *abs, const char *rel)
746
0
{
747
0
  const char *a = abs;
748
0
  const char *aslash = a;
749
0
  int up = 0;
750
0
  size_t z1, z2;
751
0
  char *s;
752
753
0
  if (rel == NULL)
754
0
    return NULL;
755
0
  if (abs == NULL || *rel == '/')
756
0
    return fz_strdup(ctx, rel);
757
758
0
  for (a = abs; *a != 0; a++)
759
0
    if (*a == '/')
760
0
      aslash = a+1;
761
762
0
  while (rel[0] == '.')
763
0
  {
764
0
    if (rel[1] == '/')
765
0
      rel += 2;
766
0
    else if (rel[1] == '.' && rel[2] == '/')
767
0
      rel += 3, up++;
768
0
    else
769
0
      fz_throw(ctx, FZ_ERROR_FORMAT, "Unresolvable path");
770
0
  }
771
0
  if (rel[0] == 0)
772
0
    fz_throw(ctx, FZ_ERROR_FORMAT, "Unresolvable path");
773
774
0
  while (up)
775
0
  {
776
0
    while (aslash != abs && aslash[-1] != '/')
777
0
      aslash--;
778
779
0
    up--;
780
0
  }
781
782
0
  z1 = aslash - abs;
783
0
  z2 = strlen(rel);
784
0
  s = fz_malloc(ctx, z1 + z2 + 1);
785
0
  if (z1)
786
0
    memcpy(s, abs, z1);
787
0
  memcpy(s+z1, rel, z2+1);
788
789
0
  return s;
790
0
}
791
792
static char *
793
collate_t_content(fz_context *ctx, fz_xml *top)
794
0
{
795
0
  char *val = NULL;
796
0
  fz_xml *next;
797
0
  fz_xml *pos = fz_xml_down(top);
798
799
0
  while (pos != top)
800
0
  {
801
    /* Capture all the 't' content. */
802
0
    if (fz_xml_is_tag(pos, "t"))
803
0
    {
804
      /* Remember the content. */
805
0
      char *s = fz_xml_text(fz_xml_down(pos));
806
807
0
      if (s == NULL)
808
0
      {
809
        /* Do nothing */
810
0
      }
811
0
      else if (val == NULL)
812
0
        val = fz_strdup(ctx, s);
813
0
      else
814
0
      {
815
0
        char *val2;
816
0
        size_t z1 = strlen(val);
817
0
        size_t z2 = strlen(s) + 1;
818
0
        fz_try(ctx)
819
0
        {
820
0
          val2 = fz_malloc(ctx, z1 + z2);
821
0
        }
822
0
        fz_catch(ctx)
823
0
        {
824
0
          fz_free(ctx, val);
825
0
          fz_rethrow(ctx);
826
0
        }
827
0
        memcpy(val2, val, z1);
828
0
        memcpy(val2 + z1, s, z2);
829
0
        fz_free(ctx, val);
830
0
        val = val2;
831
0
      }
832
      /* Do NOT go down, we've already dealt with that. */
833
0
    }
834
0
    else if (fz_xml_is_tag(pos, "rPr") || fz_xml_is_tag(pos, "rPh"))
835
0
    {
836
      /* We do not want the 't' content from within these. */
837
0
    }
838
0
    else
839
0
    {
840
      /* Try to move down. */
841
0
      next = fz_xml_down(pos);
842
0
      if (next)
843
0
      {
844
        /* We can move down, easy! */
845
0
        pos = next;
846
0
        continue;
847
0
      }
848
0
    }
849
    /* Try moving to next. */
850
0
    next = fz_xml_next(pos);
851
0
    if (next)
852
0
    {
853
      /* We can move to next, easy! */
854
0
      pos = next;
855
0
      continue;
856
0
    }
857
858
    /* If we can't go down, or next, pop up until we
859
     * find somewhere we can go next from. */
860
0
    while (1)
861
0
    {
862
      /* OK. So move up. */
863
0
      pos = fz_xml_up(pos);
864
      /* Check for hitting the top. */
865
0
      if (pos == top)
866
0
        break;
867
0
      next = fz_xml_next(pos);
868
0
      if (next)
869
0
      {
870
0
        pos = next;
871
0
        break;
872
0
      }
873
0
    }
874
0
  }
875
876
0
  return val;
877
0
}
878
879
static fz_xml *
880
try_parse_xml_archive_entry(fz_context *ctx, fz_archive *arch, const char *filename, int preserve_white)
881
0
{
882
0
  if (!fz_has_archive_entry(ctx, arch, filename))
883
0
    return NULL;
884
885
0
  return fz_parse_xml_archive_entry(ctx, arch, filename, preserve_white);
886
0
}
887
888
static void
889
load_shared_strings(fz_context *ctx, fz_archive *arch, fz_xml *rels, doc_info *info, const char *file)
890
0
{
891
0
  fz_xml *pos = fz_xml_find_dfs(rels, "Relationship", "Type", "http://schemas.openxmlformats.org/officeDocument/2006/relationships/sharedStrings");
892
0
  const char *ss_file = fz_xml_att(pos, "Target");
893
0
  char *resolved = NULL;
894
0
  fz_xml *xml = NULL;
895
896
0
  if (ss_file == NULL)
897
0
    return;
898
899
0
  fz_var(xml);
900
0
  fz_var(resolved);
901
902
0
  fz_try(ctx)
903
0
  {
904
0
    resolved = make_absolute_path(ctx, file, ss_file);
905
0
    xml = fz_parse_xml_archive_entry(ctx, arch, resolved, 1);
906
907
0
    pos = fz_xml_find_dfs(xml, "si", NULL, NULL);
908
0
    while (pos)
909
0
    {
910
0
      char **ss = fz_push_list(ctx, info->shared_strings);
911
912
0
      *ss = collate_t_content(ctx, pos);
913
914
0
      pos = fz_xml_find_next_dfs(pos, "si", NULL, NULL);
915
0
    }
916
0
  }
917
0
  fz_always(ctx)
918
0
  {
919
0
    fz_drop_xml(ctx, xml);
920
0
    fz_free(ctx, resolved);
921
0
  }
922
0
  fz_catch(ctx)
923
0
    fz_rethrow(ctx);
924
0
}
925
926
static void
927
load_footnotes(fz_context *ctx, fz_archive *arch, fz_xml *rels, doc_info *info, const char *file)
928
0
{
929
0
  char *resolved = NULL;
930
0
  fz_xml *xml = NULL;
931
0
  char *str = NULL;
932
933
0
  fz_var(xml);
934
0
  fz_var(str);
935
0
  fz_var(resolved);
936
937
0
  fz_try(ctx)
938
0
  {
939
0
    fz_xml *pos;
940
941
0
    resolved = make_absolute_path(ctx, file, "footnotes.xml");
942
0
    xml = try_parse_xml_archive_entry(ctx, arch, resolved, 1);
943
0
    if (xml == NULL)
944
0
      break;
945
946
0
    pos = fz_xml_find_dfs(xml, "footnote", NULL, NULL);
947
0
    while (pos)
948
0
    {
949
0
      int n = fz_atoi(fz_xml_att(pos, "w:id"));
950
951
0
      if (n >= 0)
952
0
      {
953
0
        str = collate_t_content(ctx, pos);
954
0
        if (str)
955
0
        {
956
0
          if (n >= info->footnotes_len)
957
0
            fz_extend_list(ctx, info->footnotes, n+1 - info->footnotes_len);
958
959
0
          info->footnotes[n] = str;
960
0
          str = NULL;
961
0
        }
962
0
      }
963
0
      pos = fz_xml_find_next_dfs(pos, "footnote", NULL, NULL);
964
0
    }
965
0
  }
966
0
  fz_always(ctx)
967
0
  {
968
0
    fz_drop_xml(ctx, xml);
969
0
    fz_free(ctx, resolved);
970
0
    fz_free(ctx, str);
971
0
  }
972
0
  fz_catch(ctx)
973
0
    fz_rethrow(ctx);
974
0
}
975
976
static void
977
process_office_document(fz_context *ctx, fz_archive *arch, const char *file, doc_info *info)
978
0
{
979
0
  char *file_rels;
980
0
  fz_xml *xml = NULL;
981
0
  fz_xml *rels = NULL;
982
0
  char *resolved_rel = NULL;
983
984
0
  if (file == NULL)
985
0
    return;
986
987
0
  file_rels = make_rel_name(ctx, file);
988
989
0
  fz_var(resolved_rel);
990
991
0
  fz_var(rels);
992
0
  fz_var(xml);
993
994
0
  fz_try(ctx)
995
0
  {
996
0
    fz_xml *pos;
997
998
0
    rels = fz_parse_xml_archive_entry(ctx, arch, file_rels, 0);
999
0
    xml = fz_parse_xml_archive_entry(ctx, arch, file, 1);
1000
1001
    /* XLSX */
1002
0
    pos = fz_xml_find_dfs(xml, "sheet", NULL, NULL);
1003
0
    if (pos)
1004
0
    {
1005
0
      load_shared_strings(ctx, arch, rels, info, file);
1006
0
      while (pos)
1007
0
      {
1008
0
        char *name = fz_xml_att(pos, "name");
1009
0
        char *id = fz_xml_att(pos, "r:id");
1010
0
        char *sheet = lookup_rel(ctx, rels, id);
1011
1012
0
        if (sheet)
1013
0
        {
1014
0
          resolved_rel = make_absolute_path(ctx, file, sheet);
1015
0
          process_sheet(ctx, arch, name, resolved_rel, info);
1016
0
          fz_free(ctx, resolved_rel);
1017
0
          resolved_rel = NULL;
1018
0
        }
1019
0
        pos = fz_xml_find_next_dfs(pos, "sheet", NULL, NULL);
1020
0
      }
1021
0
      break;
1022
0
    }
1023
1024
    /* Let's try it as a powerpoint */
1025
0
    pos = fz_xml_find_dfs(xml, "sldId", NULL, NULL);
1026
0
    if (pos)
1027
0
    {
1028
0
      while (pos)
1029
0
      {
1030
0
        char *id = fz_xml_att(pos, "r:id");
1031
0
        char *sheet = lookup_rel(ctx, rels, id);
1032
1033
0
        if (sheet)
1034
0
        {
1035
0
          resolved_rel = make_absolute_path(ctx, file, sheet);
1036
0
          process_slide(ctx, arch, resolved_rel, info);
1037
0
          fz_free(ctx, resolved_rel);
1038
0
          resolved_rel = NULL;
1039
0
        }
1040
0
        pos = fz_xml_find_next_dfs(pos, "sldId", NULL, NULL);
1041
0
      }
1042
0
      break;
1043
0
    }
1044
1045
    /* Let's try it as word. */
1046
0
    {
1047
0
      load_footnotes(ctx, arch, rels, info, file);
1048
0
      process_doc_stream(ctx, xml, info, 1);
1049
0
    }
1050
0
  }
1051
0
  fz_always(ctx)
1052
0
  {
1053
0
    fz_drop_xml(ctx, xml);
1054
0
    fz_drop_xml(ctx, rels);
1055
0
    fz_free(ctx, resolved_rel);
1056
0
    fz_free(ctx, file_rels);
1057
0
  }
1058
0
  fz_catch(ctx)
1059
0
    fz_rethrow(ctx);
1060
0
}
1061
1062
static void
1063
process_office_document_properties(fz_context *ctx, fz_archive *arch, const char *file, doc_info *info)
1064
0
{
1065
0
  fz_xml *xml = NULL;
1066
0
  char *title;
1067
1068
0
  fz_var(xml);
1069
1070
0
  fz_try(ctx)
1071
0
  {
1072
0
    fz_xml *pos;
1073
1074
0
    xml = fz_parse_xml_archive_entry(ctx, arch, file, 1);
1075
1076
0
    pos = fz_xml_find_dfs(xml, "title", NULL, NULL);
1077
0
    title = fz_xml_text(fz_xml_down(pos));
1078
0
    if (title)
1079
0
    {
1080
0
      fz_write_string(ctx, info->out, "<title>");
1081
0
      doc_escape(ctx, info->out, title);
1082
0
      fz_write_string(ctx, info->out, "</title>");
1083
0
    }
1084
0
  }
1085
0
  fz_always(ctx)
1086
0
  {
1087
0
    fz_drop_xml(ctx, xml);
1088
0
  }
1089
0
  fz_catch(ctx)
1090
0
    fz_rethrow(ctx);
1091
0
}
1092
1093
static fz_buffer *
1094
fz_office_to_html(fz_context *ctx, fz_html_font_set *set, fz_buffer *buffer_in, fz_archive *dir, fz_office_to_html_opts *opts)
1095
0
{
1096
0
  fz_stream *stream = NULL;
1097
0
  fz_archive *archive = NULL;
1098
0
  fz_buffer *buffer_out = NULL;
1099
0
  fz_xml *xml = NULL;
1100
0
  fz_xml *pos = NULL;
1101
0
  fz_xml *rels = NULL;
1102
0
  const char *schema = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/officeDocument";
1103
0
  const char *schema_props = "http://schemas.openxmlformats.org/package/2006/relationships/metadata/core-properties";
1104
0
  doc_info info = { 0 };
1105
0
  int i;
1106
1107
0
  fz_var(archive);
1108
0
  fz_var(stream);
1109
0
  fz_var(buffer_out);
1110
0
  fz_var(xml);
1111
0
  fz_var(rels);
1112
1113
0
  if (opts)
1114
0
    info.opts = *opts;
1115
1116
0
  fz_try(ctx)
1117
0
  {
1118
0
    if (buffer_in)
1119
0
    {
1120
0
      stream = fz_open_buffer(ctx, buffer_in);
1121
0
      archive = fz_open_archive_with_stream(ctx, stream);
1122
0
    }
1123
0
    else
1124
0
      archive = fz_keep_archive(ctx, dir);
1125
0
    buffer_out = fz_new_buffer(ctx, 1024);
1126
0
    info.out = fz_new_output_with_buffer(ctx, buffer_out);
1127
1128
    /* Is it an HWPX ?*/
1129
0
    xml = try_parse_xml_archive_entry(ctx, archive, "META-INF/container.xml", 0);
1130
0
    if (xml)
1131
0
    {
1132
0
      pos = fz_xml_find_dfs(xml, "rootfile", "media-type", "application/hwpml-package+xml");
1133
0
      if (!pos)
1134
0
        fz_throw(ctx, FZ_ERROR_FORMAT, "Archive not hwpx.");
1135
1136
0
      while (pos)
1137
0
      {
1138
0
        const char *file = fz_xml_att(pos, "full-path");
1139
0
        process_rootfile(ctx, archive, file, &info);
1140
0
        pos = fz_xml_find_next_dfs(pos, "rootfile", "media-type", "application/hwpml-package+xml");
1141
0
      }
1142
0
      fz_close_output(ctx, info.out);
1143
0
      break;
1144
0
    }
1145
1146
    /* Try other types */
1147
0
    {
1148
0
      xml = try_parse_xml_archive_entry(ctx, archive, "_rels/.rels", 0);
1149
1150
0
      fz_write_string(ctx, info.out, "<html>\n");
1151
1152
0
      pos = fz_xml_find_dfs(xml, "Relationship", "Type", schema_props);
1153
0
      if (pos)
1154
0
      {
1155
0
        const char *file = fz_xml_att(pos, "Target");
1156
0
        fz_write_string(ctx, info.out, "<head>\n");
1157
0
        process_office_document_properties(ctx, archive, file, &info);
1158
0
        fz_write_string(ctx, info.out, "</head>\n");
1159
0
      }
1160
1161
0
      fz_write_string(ctx, info.out, "<body>\n");
1162
0
      pos = fz_xml_find_dfs(xml, "Relationship", "Type", schema);
1163
0
      if (!pos)
1164
0
        fz_throw(ctx, FZ_ERROR_FORMAT, "Archive not docx.");
1165
1166
0
      while (pos)
1167
0
      {
1168
0
        const char *file = fz_xml_att(pos, "Target");
1169
0
        if (file)
1170
0
          process_office_document(ctx, archive, file, &info);
1171
0
        pos = fz_xml_find_next_dfs(pos, "Relationship", "Type", schema);
1172
0
      }
1173
0
    }
1174
1175
0
    fz_close_output(ctx, info.out);
1176
0
  }
1177
0
  fz_always(ctx)
1178
0
  {
1179
0
    fz_drop_xml(ctx, rels);
1180
0
    fz_drop_xml(ctx, xml);
1181
0
    for (i = 0; i < info.shared_strings_len; ++i)
1182
0
      fz_free(ctx, info.shared_strings[i]);
1183
0
    fz_free(ctx, info.shared_strings);
1184
0
    for (i = 0; i < info.footnotes_len; ++i)
1185
0
      fz_free(ctx, info.footnotes[i]);
1186
0
    fz_free(ctx, info.footnotes);
1187
0
    fz_drop_output(ctx, info.out);
1188
0
    fz_free(ctx, info.label);
1189
0
    fz_drop_archive(ctx, archive);
1190
0
    fz_drop_stream(ctx, stream);
1191
0
  }
1192
0
  fz_catch(ctx)
1193
0
  {
1194
0
    fz_drop_buffer(ctx, buffer_out);
1195
0
    fz_rethrow(ctx);
1196
0
  }
1197
1198
#ifdef DEBUG_OFFICE_TO_HTML
1199
  {
1200
    unsigned char *storage;
1201
    size_t len = fz_buffer_storage(ctx, buffer_out, &storage);
1202
    fz_write_printf(ctx, fz_stddbg(ctx), "fz_office_to_html: Output buffer, len=%zd:\n", len);
1203
    fz_write_buffer(ctx, fz_stddbg(ctx), buffer_out);
1204
  }
1205
#endif
1206
1207
0
  return buffer_out;
1208
0
}
1209
1210
/* Office document handler */
1211
1212
static fz_buffer *
1213
office_to_html(fz_context *ctx, fz_html_font_set *set, fz_buffer *buf, fz_archive *zip)
1214
0
{
1215
0
  fz_office_to_html_opts opts = { 0 };
1216
1217
0
  return fz_office_to_html(ctx, set, buf, zip, &opts);
1218
0
}
1219
1220
static const fz_htdoc_format fz_htdoc_office =
1221
{
1222
  "Office document",
1223
  office_to_html,
1224
  0, 1,
1225
  FZ_HTML_FLAVOR_DEFAULT
1226
};
1227
1228
static fz_document *
1229
office_open_document(fz_context *ctx, const fz_document_handler *handler, fz_stream *file, fz_stream *accel, fz_archive *zip, void *state)
1230
0
{
1231
0
  return fz_htdoc_open_document_with_stream_and_dir(ctx, file, zip, &fz_htdoc_office);
1232
0
}
1233
1234
static const char *office_extensions[] =
1235
{
1236
  "docx",
1237
  "xlsx",
1238
  "pptx",
1239
  "hwpx",
1240
  NULL
1241
};
1242
1243
static const char *office_mimetypes[] =
1244
{
1245
  // DOCX
1246
  "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
1247
  // XLSX
1248
  "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
1249
  // PPTX
1250
  "application/vnd.openxmlformats-officedocument.presentationml.presentation",
1251
  // HWPX
1252
  "application/haansofthwpx",
1253
  "application/vnd.hancom.hwpx",
1254
  NULL
1255
};
1256
1257
/* We are only ever 75% sure here, to allow a 'better' handler, such as sodochandler
1258
 * to override us by returning 100. */
1259
static int
1260
office_recognize_doc_content(fz_context *ctx, const fz_document_handler *handler, fz_stream *stream, fz_archive *zip, void **state, fz_document_recognize_state_free_fn **free_state)
1261
67
{
1262
67
  fz_archive *arch = NULL;
1263
67
  int ret = 0;
1264
67
  fz_xml *xml = NULL;
1265
1266
67
  if (state)
1267
67
    *state = NULL;
1268
67
  if (free_state)
1269
67
    *free_state = NULL;
1270
1271
67
  fz_var(arch);
1272
67
  fz_var(ret);
1273
67
  fz_var(xml);
1274
1275
134
  fz_try(ctx)
1276
134
  {
1277
67
    if (stream)
1278
67
    {
1279
67
      arch = fz_try_open_archive_with_stream(ctx, stream);
1280
67
      if (arch == NULL)
1281
67
        break;
1282
67
    }
1283
0
    else
1284
0
      arch = fz_keep_archive(ctx, zip);
1285
1286
0
    xml = fz_try_parse_xml_archive_entry(ctx, arch, "META-INF/container.xml", 0);
1287
0
    if (xml)
1288
0
    {
1289
0
      if (fz_xml_find_dfs(xml, "rootfile", "media-type", "application/hwpml-package+xml"))
1290
0
        ret = 75; /* HWPX */
1291
0
      break;
1292
0
    }
1293
0
    xml = fz_try_parse_xml_archive_entry(ctx, arch, "_rels/.rels", 0);
1294
0
    if (xml)
1295
0
    {
1296
0
      if (fz_xml_find_dfs(xml, "Relationship", "Type", "http://schemas.openxmlformats.org/officeDocument/2006/relationships/officeDocument"))
1297
0
      {
1298
0
        ret = 75; /* DOCX | PPTX | XLSX */
1299
0
      }
1300
0
      break;
1301
0
    }
1302
0
  }
1303
134
  fz_always(ctx)
1304
67
  {
1305
67
    fz_drop_xml(ctx, xml);
1306
67
    fz_drop_archive(ctx, arch);
1307
67
  }
1308
67
  fz_catch(ctx)
1309
0
    fz_rethrow(ctx);
1310
1311
67
  return ret;
1312
67
}
1313
1314
fz_document_handler office_document_handler =
1315
{
1316
  NULL,
1317
  office_open_document,
1318
  office_extensions,
1319
  office_mimetypes,
1320
  office_recognize_doc_content
1321
};