Coverage Report

Created: 2026-11-12 07:07

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
/* Google docs maxes columns out at ZZZ = 18278.
514
 * Excel (and similar) max out at XFD = 16384.
515
 */
516
0
#define MAX_GOOGLE_DOC_COLUMN 18278
517
518
static int
519
col_from_label(const char *label)
520
0
{
521
0
  int col = 0;
522
0
  int len = 26;
523
0
  int base = 0;
524
525
  /* If we can't read the column, return 0. */
526
0
  if (label == NULL || *label < 'A' || *label > 'Z')
527
0
    return 0;
528
529
  /*  Each section (A-Z, AA-ZZ, AAA-ZZZ etc) is of len 'len', and starts
530
   *  at base index 'base'. Each section is 26 times as long, and starts
531
   *  at base + len from the previous section.
532
   *
533
   *  A:  col = 26 * 0 + 0 + 0
534
   *  AA: col = (26 * 0 + 0 + 0) * 26 + 0 + 26 = 26
535
   *  AAA:  col = (((26 * 0 + 0 + 0) * 26 + 0 + 26)*26 + 0 + 26*26 = 26 + 26 * 26
536
   */
537
0
  do
538
0
  {
539
0
    col = 26 * col + (*label++) - 'A' + base;
540
0
    if (col > MAX_GOOGLE_DOC_COLUMN)
541
0
      return 0;
542
0
    base += len;
543
0
    len *= 26;
544
0
  }
545
0
  while (*label >= 'A' && *label <= 'Z');
546
547
0
  return col+1;
548
0
}
549
550
static void
551
show_cell_text(fz_context *ctx, fz_xml *top, doc_info *info)
552
0
{
553
0
  fz_xml *pos = top;
554
0
  fz_xml *next;
555
556
0
  while (pos)
557
0
  {
558
0
    char *text = fz_xml_text(pos);
559
560
0
    if (text)
561
0
    {
562
0
      send_cell_formatting(ctx, info);
563
0
      doc_escape(ctx, info->out, text);
564
0
    }
565
566
    /* Always try to move down. */
567
0
    next = fz_xml_down(pos);
568
0
    if (next)
569
0
    {
570
      /* We can move down, easy! */
571
0
      pos = next;
572
0
      continue;
573
0
    }
574
575
0
    if (pos == top)
576
0
      break;
577
578
    /* We can't move down, try moving to next. */
579
0
    next = fz_xml_next(pos);
580
0
    if (next)
581
0
    {
582
      /* We can move to next, easy! */
583
0
      pos = next;
584
0
      continue;
585
0
    }
586
587
    /* If we can't go down, or next, pop up until we
588
     * find somewhere we can go next from. */
589
0
    while (1)
590
0
    {
591
      /* OK. So move up. */
592
0
      pos = fz_xml_up(pos);
593
      /* Check for hitting the top. */
594
0
      if (pos == top)
595
0
        pos = NULL;
596
0
      if (pos == NULL)
597
0
        break;
598
0
      next = fz_xml_next(pos);
599
0
      if (next)
600
0
      {
601
0
        pos = next;
602
0
        break;
603
0
      }
604
0
    }
605
0
  }
606
0
}
607
608
static void
609
arrived_at_cell(fz_context *ctx, doc_info *info, const char *label)
610
0
{
611
0
  int col;
612
613
  /* If we have a label queued, and no label is given here, then we're
614
   * processing a 'cell' callback after having had a 'cellname'
615
   * callback. So don't signal it twice! */
616
0
  if (label == NULL && info->label)
617
0
    return;
618
619
0
  col = label ? col_from_label(label) : 0;
620
621
0
  fz_free(ctx, info->label);
622
0
  info->label = NULL;
623
0
  info->label = label ? fz_strdup(ctx, label) : NULL;
624
0
  info->col_at = col;
625
0
}
626
627
static void
628
show_cell(fz_context *ctx, fz_xml *cell, doc_info *info)
629
0
{
630
0
  char *t = fz_xml_att(cell, "t");
631
0
  fz_xml *v = fz_xml_find_down(cell, "v");
632
0
  const char *r = fz_xml_att(cell, "r");
633
634
0
  arrived_at_cell(ctx, info, r);
635
0
  if (t && t[0] == 's' && t[1] == 0)
636
0
    show_shared_string(ctx, v, info);
637
0
  else
638
0
    show_cell_text(ctx, v, info);
639
0
}
640
641
static void
642
new_row(fz_context *ctx, doc_info *info)
643
0
{
644
0
  if (info->col_signalled)
645
0
  {
646
    /* We've sent at least one cell. So need to close the
647
     * td and tr */
648
0
    fz_write_string(ctx, info->out, "</td>\n</tr>\n");
649
0
  }
650
0
  else
651
0
  {
652
    /* We've not sent anything for this row. Keep the counts
653
     * correct. */
654
0
    fz_write_string(ctx, info->out, "<tr></tr>\n");
655
0
  }
656
0
  info->col_at = 1;
657
0
  info->col_signalled = 0;
658
0
  fz_free(ctx, info->label);
659
0
  info->label = NULL;
660
0
}
661
662
static void
663
process_sheet(fz_context *ctx, fz_archive *arch, const char *name, const char *file, doc_info *info)
664
0
{
665
0
  fz_xml *xml = fz_parse_xml_archive_entry(ctx, arch, file, 1);
666
667
#ifdef DEBUG_OFFICE_TO_HTML
668
  fz_write_printf(ctx, fz_stddbg(ctx), "process_sheet:\n");
669
  fz_output_xml(ctx, fz_stddbg(ctx), xml, 0);
670
#endif
671
672
0
  fz_write_printf(ctx, info->out, "<table id=\"%s\">\n", name);
673
674
0
  info->sheet_name = name;
675
0
  info->col_at = 0;
676
0
  info->col_signalled = 0;
677
678
0
  fz_try(ctx)
679
0
  {
680
0
    fz_xml *pos = xml;
681
0
    fz_xml *next;
682
683
0
    while (pos)
684
0
    {
685
      /* When we arrive on a node, check if it's a cell. */
686
0
      if (fz_xml_is_tag(pos, "c"))
687
0
      {
688
0
        show_cell(ctx, pos, info);
689
        /* Do NOT go down, we've already dealt with that. */
690
0
      }
691
0
      else
692
0
      {
693
        /* Try to move down. */
694
0
        next = fz_xml_down(pos);
695
0
        if (next)
696
0
        {
697
          /* We can move down, easy! */
698
0
          pos = next;
699
0
          continue;
700
0
        }
701
0
      }
702
      /* Try moving to next. */
703
0
      next = fz_xml_next(pos);
704
0
      if (next)
705
0
      {
706
        /* We can move to next, easy! */
707
0
        pos = next;
708
0
        continue;
709
0
      }
710
711
      /* If we can't go down, or next, pop up until we
712
       * find somewhere we can go next from. */
713
0
      while (1)
714
0
      {
715
        /* OK. So move up. */
716
0
        pos = fz_xml_up(pos);
717
        /* Check for hitting the top. */
718
0
        if (pos == NULL)
719
0
          break;
720
721
        /* We've returned to a node. See if it's a 'row'. */
722
0
        if (fz_xml_is_tag(pos, "row"))
723
0
          new_row(ctx, info);
724
725
0
        next = fz_xml_next(pos);
726
0
        if (next)
727
0
        {
728
0
          pos = next;
729
0
          break;
730
0
        }
731
0
      }
732
0
    }
733
0
    if (info->col_signalled)
734
0
      fz_write_printf(ctx, info->out, "</td>\n</tr>\n");
735
0
    fz_write_printf(ctx, info->out, "</table>\n");
736
0
  }
737
0
  fz_always(ctx)
738
0
    fz_drop_xml(ctx, xml);
739
0
  fz_catch(ctx)
740
0
    fz_rethrow(ctx);
741
0
}
742
743
static void
744
process_slide(fz_context *ctx, fz_archive *arch, const char *file, doc_info *info)
745
0
{
746
0
  fz_write_printf(ctx, info->out, "<div id=\"slide%d\">\n", info->page++);
747
0
  process_item(ctx, arch, file, info, 0);
748
0
  fz_write_printf(ctx, info->out, "</div>\n");
749
0
}
750
751
static char *
752
make_absolute_path(fz_context *ctx, const char *abs, const char *rel)
753
0
{
754
0
  const char *a = abs;
755
0
  const char *aslash = a;
756
0
  int up = 0;
757
0
  size_t z1, z2;
758
0
  char *s;
759
760
0
  if (rel == NULL)
761
0
    return NULL;
762
0
  if (abs == NULL || *rel == '/')
763
0
    return fz_strdup(ctx, rel);
764
765
0
  for (a = abs; *a != 0; a++)
766
0
    if (*a == '/')
767
0
      aslash = a+1;
768
769
0
  while (rel[0] == '.')
770
0
  {
771
0
    if (rel[1] == '/')
772
0
      rel += 2;
773
0
    else if (rel[1] == '.' && rel[2] == '/')
774
0
      rel += 3, up++;
775
0
    else
776
0
      fz_throw(ctx, FZ_ERROR_FORMAT, "Unresolvable path");
777
0
  }
778
0
  if (rel[0] == 0)
779
0
    fz_throw(ctx, FZ_ERROR_FORMAT, "Unresolvable path");
780
781
0
  while (up)
782
0
  {
783
0
    while (aslash != abs && aslash[-1] != '/')
784
0
      aslash--;
785
786
0
    up--;
787
0
  }
788
789
0
  z1 = aslash - abs;
790
0
  z2 = strlen(rel);
791
0
  s = fz_malloc(ctx, z1 + z2 + 1);
792
0
  if (z1)
793
0
    memcpy(s, abs, z1);
794
0
  memcpy(s+z1, rel, z2+1);
795
796
0
  return s;
797
0
}
798
799
static char *
800
collate_t_content(fz_context *ctx, fz_xml *top)
801
0
{
802
0
  char *val = NULL;
803
0
  fz_xml *next;
804
0
  fz_xml *pos = fz_xml_down(top);
805
806
0
  while (pos != top)
807
0
  {
808
    /* Capture all the 't' content. */
809
0
    if (fz_xml_is_tag(pos, "t"))
810
0
    {
811
      /* Remember the content. */
812
0
      char *s = fz_xml_text(fz_xml_down(pos));
813
814
0
      if (s == NULL)
815
0
      {
816
        /* Do nothing */
817
0
      }
818
0
      else if (val == NULL)
819
0
        val = fz_strdup(ctx, s);
820
0
      else
821
0
      {
822
0
        char *val2;
823
0
        size_t z1 = strlen(val);
824
0
        size_t z2 = strlen(s) + 1;
825
0
        fz_try(ctx)
826
0
        {
827
0
          val2 = fz_malloc(ctx, z1 + z2);
828
0
        }
829
0
        fz_catch(ctx)
830
0
        {
831
0
          fz_free(ctx, val);
832
0
          fz_rethrow(ctx);
833
0
        }
834
0
        memcpy(val2, val, z1);
835
0
        memcpy(val2 + z1, s, z2);
836
0
        fz_free(ctx, val);
837
0
        val = val2;
838
0
      }
839
      /* Do NOT go down, we've already dealt with that. */
840
0
    }
841
0
    else if (fz_xml_is_tag(pos, "rPr") || fz_xml_is_tag(pos, "rPh"))
842
0
    {
843
      /* We do not want the 't' content from within these. */
844
0
    }
845
0
    else
846
0
    {
847
      /* Try to move down. */
848
0
      next = fz_xml_down(pos);
849
0
      if (next)
850
0
      {
851
        /* We can move down, easy! */
852
0
        pos = next;
853
0
        continue;
854
0
      }
855
0
    }
856
    /* Try moving to next. */
857
0
    next = fz_xml_next(pos);
858
0
    if (next)
859
0
    {
860
      /* We can move to next, easy! */
861
0
      pos = next;
862
0
      continue;
863
0
    }
864
865
    /* If we can't go down, or next, pop up until we
866
     * find somewhere we can go next from. */
867
0
    while (1)
868
0
    {
869
      /* OK. So move up. */
870
0
      pos = fz_xml_up(pos);
871
      /* Check for hitting the top. */
872
0
      if (pos == top)
873
0
        break;
874
0
      next = fz_xml_next(pos);
875
0
      if (next)
876
0
      {
877
0
        pos = next;
878
0
        break;
879
0
      }
880
0
    }
881
0
  }
882
883
0
  return val;
884
0
}
885
886
static fz_xml *
887
try_parse_xml_archive_entry(fz_context *ctx, fz_archive *arch, const char *filename, int preserve_white)
888
0
{
889
0
  if (!fz_has_archive_entry(ctx, arch, filename))
890
0
    return NULL;
891
892
0
  return fz_parse_xml_archive_entry(ctx, arch, filename, preserve_white);
893
0
}
894
895
static void
896
load_shared_strings(fz_context *ctx, fz_archive *arch, fz_xml *rels, doc_info *info, const char *file)
897
0
{
898
0
  fz_xml *pos = fz_xml_find_dfs(rels, "Relationship", "Type", "http://schemas.openxmlformats.org/officeDocument/2006/relationships/sharedStrings");
899
0
  const char *ss_file = fz_xml_att(pos, "Target");
900
0
  char *resolved = NULL;
901
0
  fz_xml *xml = NULL;
902
903
0
  if (ss_file == NULL)
904
0
    return;
905
906
0
  fz_var(xml);
907
0
  fz_var(resolved);
908
909
0
  fz_try(ctx)
910
0
  {
911
0
    resolved = make_absolute_path(ctx, file, ss_file);
912
0
    xml = fz_parse_xml_archive_entry(ctx, arch, resolved, 1);
913
914
0
    pos = fz_xml_find_dfs(xml, "si", NULL, NULL);
915
0
    while (pos)
916
0
    {
917
0
      char **ss = fz_push_list(ctx, info->shared_strings);
918
919
0
      *ss = collate_t_content(ctx, pos);
920
921
0
      pos = fz_xml_find_next_dfs(pos, "si", NULL, NULL);
922
0
    }
923
0
  }
924
0
  fz_always(ctx)
925
0
  {
926
0
    fz_drop_xml(ctx, xml);
927
0
    fz_free(ctx, resolved);
928
0
  }
929
0
  fz_catch(ctx)
930
0
    fz_rethrow(ctx);
931
0
}
932
933
static void
934
load_footnotes(fz_context *ctx, fz_archive *arch, fz_xml *rels, doc_info *info, const char *file)
935
0
{
936
0
  char *resolved = NULL;
937
0
  fz_xml *xml = NULL;
938
0
  char *str = NULL;
939
940
0
  fz_var(xml);
941
0
  fz_var(str);
942
0
  fz_var(resolved);
943
944
0
  fz_try(ctx)
945
0
  {
946
0
    fz_xml *pos;
947
948
0
    resolved = make_absolute_path(ctx, file, "footnotes.xml");
949
0
    xml = try_parse_xml_archive_entry(ctx, arch, resolved, 1);
950
0
    if (xml == NULL)
951
0
      break;
952
953
0
    pos = fz_xml_find_dfs(xml, "footnote", NULL, NULL);
954
0
    while (pos)
955
0
    {
956
0
      int n = fz_atoi(fz_xml_att(pos, "w:id"));
957
958
0
      if (n < 0 || n >= INT_MAX)
959
0
      {
960
0
        fz_warn(ctx, "Footnote %d out of range", n);
961
0
      }
962
0
      else
963
0
      {
964
0
        str = collate_t_content(ctx, pos);
965
0
        if (str)
966
0
        {
967
0
          if (n >= info->footnotes_len)
968
0
            fz_extend_list(ctx, info->footnotes, n+1 - info->footnotes_len);
969
970
0
          info->footnotes[n] = str;
971
0
          str = NULL;
972
0
        }
973
0
      }
974
0
      pos = fz_xml_find_next_dfs(pos, "footnote", NULL, NULL);
975
0
    }
976
0
  }
977
0
  fz_always(ctx)
978
0
  {
979
0
    fz_drop_xml(ctx, xml);
980
0
    fz_free(ctx, resolved);
981
0
    fz_free(ctx, str);
982
0
  }
983
0
  fz_catch(ctx)
984
0
    fz_rethrow(ctx);
985
0
}
986
987
static void
988
process_office_document(fz_context *ctx, fz_archive *arch, const char *file, doc_info *info)
989
0
{
990
0
  char *file_rels;
991
0
  fz_xml *xml = NULL;
992
0
  fz_xml *rels = NULL;
993
0
  char *resolved_rel = NULL;
994
995
0
  if (file == NULL)
996
0
    return;
997
998
0
  file_rels = make_rel_name(ctx, file);
999
1000
0
  fz_var(resolved_rel);
1001
1002
0
  fz_var(rels);
1003
0
  fz_var(xml);
1004
1005
0
  fz_try(ctx)
1006
0
  {
1007
0
    fz_xml *pos;
1008
1009
0
    rels = fz_parse_xml_archive_entry(ctx, arch, file_rels, 0);
1010
0
    xml = fz_parse_xml_archive_entry(ctx, arch, file, 1);
1011
1012
    /* XLSX */
1013
0
    pos = fz_xml_find_dfs(xml, "sheet", NULL, NULL);
1014
0
    if (pos)
1015
0
    {
1016
0
      load_shared_strings(ctx, arch, rels, info, file);
1017
0
      while (pos)
1018
0
      {
1019
0
        char *name = fz_xml_att(pos, "name");
1020
0
        char *id = fz_xml_att(pos, "r:id");
1021
0
        char *sheet = lookup_rel(ctx, rels, id);
1022
1023
0
        if (sheet)
1024
0
        {
1025
0
          resolved_rel = make_absolute_path(ctx, file, sheet);
1026
0
          process_sheet(ctx, arch, name, resolved_rel, info);
1027
0
          fz_free(ctx, resolved_rel);
1028
0
          resolved_rel = NULL;
1029
0
        }
1030
0
        pos = fz_xml_find_next_dfs(pos, "sheet", NULL, NULL);
1031
0
      }
1032
0
      break;
1033
0
    }
1034
1035
    /* Let's try it as a powerpoint */
1036
0
    pos = fz_xml_find_dfs(xml, "sldId", NULL, NULL);
1037
0
    if (pos)
1038
0
    {
1039
0
      while (pos)
1040
0
      {
1041
0
        char *id = fz_xml_att(pos, "r:id");
1042
0
        char *sheet = lookup_rel(ctx, rels, id);
1043
1044
0
        if (sheet)
1045
0
        {
1046
0
          resolved_rel = make_absolute_path(ctx, file, sheet);
1047
0
          process_slide(ctx, arch, resolved_rel, info);
1048
0
          fz_free(ctx, resolved_rel);
1049
0
          resolved_rel = NULL;
1050
0
        }
1051
0
        pos = fz_xml_find_next_dfs(pos, "sldId", NULL, NULL);
1052
0
      }
1053
0
      break;
1054
0
    }
1055
1056
    /* Let's try it as word. */
1057
0
    {
1058
0
      load_footnotes(ctx, arch, rels, info, file);
1059
0
      process_doc_stream(ctx, xml, info, 1);
1060
0
    }
1061
0
  }
1062
0
  fz_always(ctx)
1063
0
  {
1064
0
    fz_drop_xml(ctx, xml);
1065
0
    fz_drop_xml(ctx, rels);
1066
0
    fz_free(ctx, resolved_rel);
1067
0
    fz_free(ctx, file_rels);
1068
0
  }
1069
0
  fz_catch(ctx)
1070
0
    fz_rethrow(ctx);
1071
0
}
1072
1073
static void
1074
process_office_document_properties(fz_context *ctx, fz_archive *arch, const char *file, doc_info *info)
1075
0
{
1076
0
  fz_xml *xml = NULL;
1077
0
  char *title;
1078
1079
0
  fz_var(xml);
1080
1081
0
  fz_try(ctx)
1082
0
  {
1083
0
    fz_xml *pos;
1084
1085
0
    xml = fz_parse_xml_archive_entry(ctx, arch, file, 1);
1086
1087
0
    pos = fz_xml_find_dfs(xml, "title", NULL, NULL);
1088
0
    title = fz_xml_text(fz_xml_down(pos));
1089
0
    if (title)
1090
0
    {
1091
0
      fz_write_string(ctx, info->out, "<title>");
1092
0
      doc_escape(ctx, info->out, title);
1093
0
      fz_write_string(ctx, info->out, "</title>");
1094
0
    }
1095
0
  }
1096
0
  fz_always(ctx)
1097
0
  {
1098
0
    fz_drop_xml(ctx, xml);
1099
0
  }
1100
0
  fz_catch(ctx)
1101
0
    fz_rethrow(ctx);
1102
0
}
1103
1104
static fz_buffer *
1105
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)
1106
0
{
1107
0
  fz_stream *stream = NULL;
1108
0
  fz_archive *archive = NULL;
1109
0
  fz_buffer *buffer_out = NULL;
1110
0
  fz_xml *xml = NULL;
1111
0
  fz_xml *pos = NULL;
1112
0
  fz_xml *rels = NULL;
1113
0
  const char *schema = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/officeDocument";
1114
0
  const char *schema_props = "http://schemas.openxmlformats.org/package/2006/relationships/metadata/core-properties";
1115
0
  doc_info info = { 0 };
1116
0
  int i;
1117
1118
0
  fz_var(archive);
1119
0
  fz_var(stream);
1120
0
  fz_var(buffer_out);
1121
0
  fz_var(xml);
1122
0
  fz_var(rels);
1123
1124
0
  if (opts)
1125
0
    info.opts = *opts;
1126
1127
0
  fz_try(ctx)
1128
0
  {
1129
0
    if (buffer_in)
1130
0
    {
1131
0
      stream = fz_open_buffer(ctx, buffer_in);
1132
0
      archive = fz_open_archive_with_stream(ctx, stream);
1133
0
    }
1134
0
    else
1135
0
      archive = fz_keep_archive(ctx, dir);
1136
0
    buffer_out = fz_new_buffer(ctx, 1024);
1137
0
    info.out = fz_new_output_with_buffer(ctx, buffer_out);
1138
1139
    /* Is it an HWPX ?*/
1140
0
    xml = try_parse_xml_archive_entry(ctx, archive, "META-INF/container.xml", 0);
1141
0
    if (xml)
1142
0
    {
1143
0
      pos = fz_xml_find_dfs(xml, "rootfile", "media-type", "application/hwpml-package+xml");
1144
0
      if (!pos)
1145
0
        fz_throw(ctx, FZ_ERROR_FORMAT, "Archive not hwpx.");
1146
1147
0
      while (pos)
1148
0
      {
1149
0
        const char *file = fz_xml_att(pos, "full-path");
1150
0
        process_rootfile(ctx, archive, file, &info);
1151
0
        pos = fz_xml_find_next_dfs(pos, "rootfile", "media-type", "application/hwpml-package+xml");
1152
0
      }
1153
0
      fz_close_output(ctx, info.out);
1154
0
      break;
1155
0
    }
1156
1157
    /* Try other types */
1158
0
    {
1159
0
      xml = try_parse_xml_archive_entry(ctx, archive, "_rels/.rels", 0);
1160
1161
0
      fz_write_string(ctx, info.out, "<html>\n");
1162
1163
0
      pos = fz_xml_find_dfs(xml, "Relationship", "Type", schema_props);
1164
0
      if (pos)
1165
0
      {
1166
0
        const char *file = fz_xml_att(pos, "Target");
1167
0
        fz_write_string(ctx, info.out, "<head>\n");
1168
0
        process_office_document_properties(ctx, archive, file, &info);
1169
0
        fz_write_string(ctx, info.out, "</head>\n");
1170
0
      }
1171
1172
0
      fz_write_string(ctx, info.out, "<body>\n");
1173
0
      pos = fz_xml_find_dfs(xml, "Relationship", "Type", schema);
1174
0
      if (!pos)
1175
0
        fz_throw(ctx, FZ_ERROR_FORMAT, "Archive not docx.");
1176
1177
0
      while (pos)
1178
0
      {
1179
0
        const char *file = fz_xml_att(pos, "Target");
1180
0
        if (file)
1181
0
          process_office_document(ctx, archive, file, &info);
1182
0
        pos = fz_xml_find_next_dfs(pos, "Relationship", "Type", schema);
1183
0
      }
1184
0
    }
1185
1186
0
    fz_close_output(ctx, info.out);
1187
0
  }
1188
0
  fz_always(ctx)
1189
0
  {
1190
0
    fz_drop_xml(ctx, rels);
1191
0
    fz_drop_xml(ctx, xml);
1192
0
    for (i = 0; i < info.shared_strings_len; ++i)
1193
0
      fz_free(ctx, info.shared_strings[i]);
1194
0
    fz_free(ctx, info.shared_strings);
1195
0
    for (i = 0; i < info.footnotes_len; ++i)
1196
0
      fz_free(ctx, info.footnotes[i]);
1197
0
    fz_free(ctx, info.footnotes);
1198
0
    fz_drop_output(ctx, info.out);
1199
0
    fz_free(ctx, info.label);
1200
0
    fz_drop_archive(ctx, archive);
1201
0
    fz_drop_stream(ctx, stream);
1202
0
  }
1203
0
  fz_catch(ctx)
1204
0
  {
1205
0
    fz_drop_buffer(ctx, buffer_out);
1206
0
    fz_rethrow(ctx);
1207
0
  }
1208
1209
#ifdef DEBUG_OFFICE_TO_HTML
1210
  {
1211
    unsigned char *storage;
1212
    size_t len = fz_buffer_storage(ctx, buffer_out, &storage);
1213
    fz_write_printf(ctx, fz_stddbg(ctx), "fz_office_to_html: Output buffer, len=%zd:\n", len);
1214
    fz_write_buffer(ctx, fz_stddbg(ctx), buffer_out);
1215
  }
1216
#endif
1217
1218
0
  return buffer_out;
1219
0
}
1220
1221
/* Office document handler */
1222
1223
static fz_buffer *
1224
office_to_html(fz_context *ctx, fz_html_font_set *set, fz_buffer *buf, fz_archive *zip)
1225
0
{
1226
0
  fz_office_to_html_opts opts = { 0 };
1227
1228
0
  return fz_office_to_html(ctx, set, buf, zip, &opts);
1229
0
}
1230
1231
static const fz_htdoc_format fz_htdoc_office =
1232
{
1233
  "Office document",
1234
  office_to_html,
1235
  0, 1,
1236
  FZ_HTML_FLAVOR_DEFAULT
1237
};
1238
1239
static fz_document *
1240
office_open_document(fz_context *ctx, const fz_document_handler *handler, fz_stream *file, fz_stream *accel, fz_archive *zip, void *state)
1241
0
{
1242
0
  return fz_htdoc_open_document_with_stream_and_dir(ctx, file, zip, &fz_htdoc_office);
1243
0
}
1244
1245
static const char *office_extensions[] =
1246
{
1247
  "docx",
1248
  "xlsx",
1249
  "pptx",
1250
  "hwpx",
1251
  NULL
1252
};
1253
1254
static const char *office_mimetypes[] =
1255
{
1256
  // DOCX
1257
  "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
1258
  // XLSX
1259
  "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
1260
  // PPTX
1261
  "application/vnd.openxmlformats-officedocument.presentationml.presentation",
1262
  // HWPX
1263
  "application/haansofthwpx",
1264
  "application/vnd.hancom.hwpx",
1265
  NULL
1266
};
1267
1268
/* We are only ever 75% sure here, to allow a 'better' handler, such as sodochandler
1269
 * to override us by returning 100. */
1270
static int
1271
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)
1272
91
{
1273
91
  fz_archive *arch = NULL;
1274
91
  int ret = 0;
1275
91
  fz_xml *xml = NULL;
1276
1277
91
  if (state)
1278
91
    *state = NULL;
1279
91
  if (free_state)
1280
91
    *free_state = NULL;
1281
1282
91
  fz_var(arch);
1283
91
  fz_var(ret);
1284
91
  fz_var(xml);
1285
1286
182
  fz_try(ctx)
1287
182
  {
1288
91
    if (stream)
1289
91
    {
1290
91
      arch = fz_try_open_archive_with_stream(ctx, stream);
1291
91
      if (arch == NULL)
1292
89
        break;
1293
91
    }
1294
0
    else
1295
0
      arch = fz_keep_archive(ctx, zip);
1296
1297
2
    xml = fz_try_parse_xml_archive_entry(ctx, arch, "META-INF/container.xml", 0);
1298
2
    if (xml)
1299
0
    {
1300
0
      if (fz_xml_find_dfs(xml, "rootfile", "media-type", "application/hwpml-package+xml"))
1301
0
        ret = 75; /* HWPX */
1302
0
      break;
1303
0
    }
1304
2
    xml = fz_try_parse_xml_archive_entry(ctx, arch, "_rels/.rels", 0);
1305
2
    if (xml)
1306
0
    {
1307
0
      if (fz_xml_find_dfs(xml, "Relationship", "Type", "http://schemas.openxmlformats.org/officeDocument/2006/relationships/officeDocument"))
1308
0
      {
1309
0
        ret = 75; /* DOCX | PPTX | XLSX */
1310
0
      }
1311
0
      break;
1312
0
    }
1313
2
  }
1314
182
  fz_always(ctx)
1315
91
  {
1316
91
    fz_drop_xml(ctx, xml);
1317
91
    fz_drop_archive(ctx, arch);
1318
91
  }
1319
91
  fz_catch(ctx)
1320
2
    fz_rethrow(ctx);
1321
1322
89
  return ret;
1323
91
}
1324
1325
fz_document_handler office_document_handler =
1326
{
1327
  NULL,
1328
  office_open_document,
1329
  office_extensions,
1330
  office_mimetypes,
1331
  office_recognize_doc_content
1332
};