Coverage Report

Created: 2026-09-06 07:18

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
/src/libe-book/src/lib/BBeBParser.cpp
Line
Count
Source
1
/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
2
/*
3
 * This file is part of the libe-book project.
4
 *
5
 * This Source Code Form is subject to the terms of the Mozilla Public
6
 * License, v. 2.0. If a copy of the MPL was not distributed with this
7
 * file, You can obtain one at http://mozilla.org/MPL/2.0/.
8
 */
9
10
#include <algorithm>
11
12
#include <librevenge-stream/librevenge-stream.h>
13
14
#include "libebook_utils.h"
15
#include "EBOOKCharsetConverter.h"
16
#include "EBOOKMemoryStream.h"
17
#include "EBOOKStreamView.h"
18
#include "EBOOKUTF8Stream.h"
19
#include "EBOOKZlibStream.h"
20
#include "BBeBMetadataParser.h"
21
#include "BBeBParser.h"
22
23
using std::string;
24
25
namespace libebook
26
{
27
28
namespace
29
{
30
31
enum Tag
32
{
33
  TAG_OBJECT_START = 0xf500,
34
  TAG_OBJECT_END = 0xf501,
35
  TAG_OBJECT_INFO_LINK = 0xf502,
36
  TAG_LINK = 0xf503,
37
  TAG_STREAM_SIZE = 0xf504,
38
  TAG_STREAM_START = 0xf505,
39
  TAG_STREAM_END = 0xf506,
40
  TAG_F507 = 0xf507,
41
  TAG_F508 = 0xf508,
42
  TAG_F509 = 0xf509,
43
  TAG_F50A = 0xf50a,
44
  TAG_CONTAINED_OBJECTS_LIST = 0xf50b,
45
  TAG_F50D = 0xf50d,
46
  TAG_F50E = 0xf50e,
47
  TAG_FONT_SIZE = 0xf511,
48
  TAG_FONT_WIDTH = 0xf512,
49
  TAG_FONT_ESCAPEMENT = 0xf513,
50
  TAG_FONT_ORIENTATION = 0xf514,
51
  TAG_FONT_WEIGHT = 0xf515,
52
  TAG_FONT_FACENAME = 0xf516,
53
  TAG_TEXT_COLOR = 0xf517,
54
  TAG_TEXT_BG_COLOR = 0xf518,
55
  TAG_WORD_SPACE = 0xf519,
56
  TAG_LETTER_SPACE = 0xf51a,
57
  TAG_BASE_LINE_SKIP = 0xf51b,
58
  TAG_LINE_SPACE = 0xf51c,
59
  TAG_PAR_INDENT = 0xf51d,
60
  TAG_PAR_SKIP = 0xf51e,
61
  TAG_TOP_MARGIN = 0xf521,
62
  TAG_F522 = 0xf522,
63
  TAG_F523 = 0xf523,
64
  TAG_ODD_SIDE_MARGIN = 0xf524,
65
  TAG_PAGE_HEIGHT = 0xf525,
66
  TAG_PAGE_WIDTH = 0xf526,
67
  TAG_F527 = 0xf527,
68
  TAG_F528 = 0xf528,
69
  TAG_F529 = 0xf529,
70
  TAG_F52A = 0xf52a,
71
  TAG_F52B = 0xf52b,
72
  TAG_EVEN_SIDE_MARGIN = 0xf52c,
73
  TAG_F52D = 0xf52d,
74
  TAG_F52E = 0xf52e,
75
  TAG_BLOCK_WIDTH = 0xf531,
76
  TAG_BLOCK_HEIGHT = 0xf532,
77
  TAG_BLOCK_RULE = 0xf533,
78
  TAG_F534 = 0xf534,
79
  TAG_F535 = 0xf535,
80
  TAG_F536 = 0xf536,
81
  TAG_F537 = 0xf537,
82
  TAG_TOP_SKIP = 0xf538,
83
  TAG_F539 = 0xf539,
84
  TAG_F53A = 0xf53a,
85
  TAG_ALIGN = 0xf53c,
86
  TAG_F53D = 0xf53d,
87
  TAG_F53E = 0xf53e,
88
  TAG_MINI_PAGE_HEIGHT = 0xf541,
89
  TAG_MINI_PAGE_WIDTH = 0xf542,
90
  TAG_F544 = 0xf544,
91
  TAG_F545 = 0xf545,
92
  TAG_LOCATION_Y = 0xf546,
93
  TAG_LOCATION_X = 0xf547,
94
  TAG_F548 = 0xf548,
95
  TAG_PUT_SOUND = 0xf549,
96
  TAG_IMAGE_CROP_RECT = 0xf54a,
97
  TAG_IMAGE_SIZE = 0xf54b,
98
  TAG_IMAGE_STREAM = 0xf54c,
99
  TAG_F54D = 0xf54d,
100
  TAG_F54E = 0xf54e,
101
  TAG_CANVAS_WIDTH = 0xf551,
102
  TAG_CANVAS_HEIGHT = 0xf552,
103
  TAG_F553 = 0xf553,
104
  TAG_STREAM_FLAGS = 0xf554,
105
  TAG_F555 = 0xf555,
106
  TAG_F556 = 0xf556,
107
  TAG_F557 = 0xf557,
108
  TAG_F558 = 0xf558,
109
  TAG_FONT_FILE_NAME = 0xf559,
110
  TAG_F55A = 0xf55a,
111
  TAG_VIEW_POINT = 0xf55b,
112
  TAG_PAGE_LIST = 0xf55c,
113
  TAG_FONT_FACE_NAME = 0xf55d,
114
  TAG_F55E = 0xf55e,
115
  TAG_F561 = 0xf561,
116
  TAG_F562 = 0xf562,
117
  TAG_F563 = 0xf563,
118
  TAG_F564 = 0xf564,
119
  TAG_F565 = 0xf565,
120
  TAG_F566 = 0xf566,
121
  TAG_F567 = 0xf567,
122
  TAG_F568 = 0xf568,
123
  TAG_F569 = 0xf569,
124
  TAG_F56A = 0xf56a,
125
  TAG_F56B = 0xf56b,
126
  TAG_JUMP_TO = 0xf56c,
127
  TAG_F56D = 0xf56d,
128
  TAG_F56E = 0xf56e,
129
  TAG_F571 = 0xf571,
130
  TAG_F572 = 0xf572,
131
  TAG_RULED_LINE = 0xf573,
132
  TAG_RUBY_ALIGN = 0xf575,
133
  TAG_RUBY_OVERHANG = 0xf576,
134
  TAG_EMPTY_DOTS_POSITION = 0xf577,
135
  TAG_EMPTY_DOTS_CODE = 0xf578,
136
  TAG_EMPTY_LINE_POSITION = 0xf579,
137
  TAG_EMPTY_LINE_MODE = 0xf57a,
138
  TAG_CHILD_PAGE_TREE = 0xf57b,
139
  TAG_PARENT_PAGE_TREE = 0xf57c,
140
  TAG_BEGIN_ITALIC = 0xf581,
141
  TAG_END_ITALIC = 0xf582,
142
  TAG_BEGIN_P = 0xf5a1,
143
  TAG_END_P = 0xf5a2,
144
  TAG_KOMA_GAIJI = 0xf5a5,
145
  TAG_KOMA_EMP_DOT_CHAR = 0xf5a6,
146
  TAG_BEGIN_BUTTON = 0xf5a7,
147
  TAG_END_BUTTON = 0xf5a8,
148
  TAG_BEGIN_RUBY = 0xf5a9,
149
  TAG_END_RUBY = 0xf5aa,
150
  TAG_BEGIN_RUBY_BASE = 0xf5ab,
151
  TAG_END_RUBY_BASE = 0xf5ac,
152
  TAG_BEGIN_RUBY_TEXT = 0xf5ad,
153
  TAG_END_RUBY_TEXT = 0xf5ae,
154
  TAG_KOMA_YOKOMOJI = 0xf5b1,
155
  TAG_F5B2 = 0xf5b2,
156
  TAG_BEGIN_TATE = 0xf5b3,
157
  TAG_END_TATE = 0xf5b4,
158
  TAG_BEGIN_NEKASE = 0xf5b5,
159
  TAG_END_NEKASE = 0xf5b6,
160
  TAG_BEGIN_SUP = 0xf5b7,
161
  TAG_END_SUP = 0xf5b8,
162
  TAG_BEGIN_SUB = 0xf5b9,
163
  TAG_END_SUB = 0xf5ba,
164
  TAG_F5BB = 0xf5bb,
165
  TAG_F5BC = 0xf5bc,
166
  TAG_F5BD = 0xf5bd,
167
  TAG_F5BE = 0xf5be,
168
  TAG_BEGIN_EMP_LINE = 0xf5c1,
169
  TAG_END_EMP_LINE = 0xf5c2,
170
  TAG_BEGIN_DRAW_CHAR = 0xf5c3,
171
  TAG_END_DRAW_CHAR = 0xf5c4,
172
  TAG_F5C5 = 0xf5c5,
173
  TAG_F5C6 = 0xf5c6,
174
  TAG_F5C7 = 0xf5c7,
175
  TAG_KOMA_AUTO_SPACING = 0xf5c8,
176
  TAG_F5C9 = 0xf5c9,
177
  TAG_SPACE = 0xf5ca,
178
  TAG_F5CB = 0xf5cb,
179
  TAG_TEXT_SIZE = 0xf5cc,
180
  TAG_KOMA_PLOT = 0xf5d1,
181
  TAG_EOL = 0xf5d2,
182
  TAG_WAIT = 0xf5d4,
183
  TAG_SOUND_STOP = 0xf5d6,
184
  TAG_MOVE_OBJ = 0xf5d7,
185
  TAG_BOOK_FONT = 0xf5d8,
186
  TAG_KOMA_PLOT_TEXT = 0xf5d9,
187
  TAG_F5DA = 0xf5da,
188
  TAG_F5DB = 0xf5db,
189
  TAG_F5DC = 0xf5dc,
190
  TAG_CHAR_SPACE = 0xf5dd,
191
  TAG_LINE_WIDTH = 0xf5f1,
192
  TAG_LINE_COLOR = 0xf5f2,
193
  TAG_FILL_COLOR = 0xf5f3,
194
  TAG_LINE_MODE = 0xf5f4,
195
  TAG_MOVE_TO = 0xf5f5,
196
  TAG_LINE_TO = 0xf5f6,
197
  TAG_DRAW_BOX = 0xf5f7,
198
  TAG_DRAW_ELLIPSE = 0xf5f8,
199
  TAG_F5F9 = 0xf5f9
200
};
201
202
// NOTE: This is just guessing. I have no idea if the stream flags are
203
// actually binary flags, or just a set of possible values.
204
enum StreamType
205
{
206
  STREAM_TYPE_BBEB_TAGS = 0,
207
  STREAM_TYPE_JPEG = BBEB_IMAGE_TYPE_JPEG,
208
  STREAM_TYPE_PNG = BBEB_IMAGE_TYPE_PNG,
209
  STREAM_TYPE_BMP = BBEB_IMAGE_TYPE_BMP,
210
  STREAM_TYPE_GIF = BBEB_IMAGE_TYPE_GIF,
211
  STREAM_TYPE_BBEB_TOC = 0x51,
212
  STREAM_TYPE_BBEB_TAGS_COMPRESSED = 0x100
213
};
214
215
class ParserException
216
{
217
};
218
219
const std::string readString(librevenge::RVNGInputStream *const input)
220
75.7k
{
221
75.7k
  const unsigned size = readU16(input);
222
223
75.7k
  EBOOKStreamView textStrm(input, input->tell(), input->tell() + int(size));
224
75.7k
  EBOOKCharsetConverter utf16("UTF-16LE");
225
75.7k
  EBOOKUTF8Stream utf8Strm(&textStrm, &utf16);
226
227
75.7k
  std::string text;
228
3.80M
  while (!utf8Strm.isEnd())
229
3.72M
    text.push_back((char) readU8(&utf8Strm));
230
231
75.7k
  return text;
232
75.7k
}
233
234
}
235
236
struct BBeBHeader
237
{
238
  BBeBHeader();
239
240
  unsigned version;
241
  uint16_t key;
242
  unsigned rootOID;
243
  uint64_t numberOfObjects;
244
  uint64_t objectIndexOffset;
245
  unsigned xmlCompSize;
246
  BBeBImageType thumbnailType;
247
  unsigned thumbnailSize;
248
  unsigned dpi;
249
  unsigned width;
250
  unsigned tocOID;
251
};
252
253
BBeBHeader::BBeBHeader()
254
6.21k
  : version(0)
255
6.21k
  , key(0)
256
6.21k
  , rootOID(0)
257
6.21k
  , numberOfObjects(0)
258
6.21k
  , objectIndexOffset(0)
259
6.21k
  , xmlCompSize(0)
260
6.21k
  , thumbnailType(BBEB_IMAGE_TYPE_UNKNOWN)
261
6.21k
  , thumbnailSize(0)
262
6.21k
  , dpi(0)
263
6.21k
  , width(0)
264
6.21k
  , tocOID(0)
265
6.21k
{
266
6.21k
}
267
268
BBeBParser::BBeBParser(librevenge::RVNGInputStream *const input, librevenge::RVNGTextInterface *const document)
269
6.21k
  : m_collector(document)
270
6.21k
  , m_input(input)
271
6.21k
  , m_header(new BBeBHeader())
272
6.21k
  , m_objectIndex()
273
6.21k
  , m_pageTree(0)
274
6.21k
  , m_toc()
275
6.21k
  , m_objectNesting(0)
276
6.21k
{
277
6.21k
}
278
279
BBeBParser::~BBeBParser()
280
6.21k
{
281
6.21k
}
282
283
bool BBeBParser::parse()
284
6.21k
{
285
6.21k
  readHeader();
286
6.21k
  readMetadata();
287
6.21k
  readThumbnail();
288
6.21k
  readObjectIndex();
289
290
6.21k
  if (0 != m_header->tocOID)
291
591
    readObject(m_header->tocOID, OBJECT_TYPE_TOC);
292
293
6.21k
  m_collector.startDocument();
294
6.21k
  readObject(m_header->rootOID, OBJECT_TYPE_BOOK_ATR);
295
6.21k
  m_collector.endDocument();
296
297
6.21k
  return false;
298
6.21k
}
299
300
bool BBeBParser::isSupported(librevenge::RVNGInputStream *const input)
301
0
{
302
0
  const unsigned char signature[] = { 'L', '\0', 'R', '\0', 'F', '\0' };
303
0
  unsigned long readBytes = 0;
304
0
  const unsigned char *s = input->read(sizeof(signature), readBytes);
305
0
  return (sizeof(signature) == readBytes) && std::equal(signature, signature + sizeof(signature), s);
306
0
}
307
308
void BBeBParser::readHeader()
309
6.21k
{
310
6.21k
  skip(m_input, 8);
311
6.21k
  m_header->version = readU16(m_input);
312
6.21k
  m_header->key = readU16(m_input);
313
6.21k
  m_header->rootOID = readU32(m_input);
314
6.21k
  m_header->numberOfObjects = readU64(m_input);
315
6.21k
  m_header->objectIndexOffset = readU64(m_input);
316
6.21k
  skip(m_input, 6);
317
6.21k
  m_header->dpi = readU16(m_input);
318
6.21k
  if (0 == m_header->dpi)
319
1.01k
  {
320
1.01k
    EBOOK_DEBUG_MSG(("DPI is 0, likely a broken file\n"));
321
1.01k
    m_header->dpi = 1660;
322
1.01k
  }
323
6.21k
  skip(m_input, 2);
324
6.21k
  m_header->width = readU16(m_input);
325
6.21k
  skip(m_input, 24);
326
6.21k
  m_header->tocOID = readU32(m_input);
327
6.21k
  skip(m_input, 4);
328
6.21k
  m_header->xmlCompSize = readU16(m_input);
329
330
6.21k
  if (800 <= m_header->version)
331
4.33k
  {
332
4.33k
    const unsigned thumbnailType = readU16(m_input);
333
4.33k
    switch (thumbnailType)
334
4.33k
    {
335
2.71k
    case BBEB_IMAGE_TYPE_JPEG :
336
2.72k
    case BBEB_IMAGE_TYPE_PNG :
337
2.72k
    case BBEB_IMAGE_TYPE_BMP :
338
2.72k
    case BBEB_IMAGE_TYPE_GIF :
339
2.72k
      m_header->thumbnailType = static_cast<BBeBImageType>(thumbnailType);
340
2.72k
      break;
341
1.59k
    default :
342
1.59k
      EBOOK_DEBUG_MSG(("unknown thumbnail type %x\n", thumbnailType));
343
1.59k
      break;
344
4.33k
    }
345
4.32k
    m_header->thumbnailSize = readU32(m_input);
346
4.32k
  }
347
6.21k
}
348
349
void BBeBParser::readMetadata()
350
6.13k
{
351
6.13k
  skip(m_input, 4); // uncompressed size?
352
6.13k
  unsigned const char *const data = readNBytes(m_input, m_header->xmlCompSize);
353
6.13k
  EBOOKMemoryStream memoryStrm(data, m_header->xmlCompSize);
354
6.13k
  EBOOKZlibStream zlibStrm(&memoryStrm);
355
356
6.13k
  BBeBMetadataParser parser(&zlibStrm);
357
6.13k
  parser.parse();
358
359
6.13k
  m_collector.collectMetadata(parser.getMetadata());
360
6.13k
}
361
362
void BBeBParser::readThumbnail()
363
5.49k
{
364
  // TODO: implement me
365
5.49k
}
366
367
void BBeBParser::readObjectIndex()
368
5.49k
{
369
5.49k
  m_input->seek((long) m_header->objectIndexOffset, librevenge::RVNG_SEEK_SET);
370
256k
  for (uint64_t i = m_header->numberOfObjects; 0 != i; --i)
371
250k
  {
372
250k
    unsigned id = readU32(m_input);
373
250k
    BBeBIndexEntry entry;
374
250k
    entry.offset = readU32(m_input);
375
250k
    entry.size = readU32(m_input);
376
250k
    entry.read = false;
377
250k
    entry.reading = false;
378
250k
    m_objectIndex.insert(ObjectIndex_t::value_type(id, entry));
379
250k
    skip(m_input, 4);
380
250k
  }
381
5.49k
}
382
383
namespace
384
{
385
386
const unsigned BBEB_MAX_OBJECT_NESTING = 1 << 10;
387
388
struct NestingGuard
389
{
390
84.6k
  explicit NestingGuard(unsigned &nesting) : m_nesting(nesting)
391
84.6k
  {
392
84.6k
    ++m_nesting;
393
84.6k
  }
394
  ~NestingGuard()
395
84.6k
  {
396
84.6k
    --m_nesting;
397
84.6k
  }
398
  unsigned &m_nesting;
399
private:
400
  NestingGuard(const NestingGuard &);
401
  NestingGuard &operator=(const NestingGuard &);
402
};
403
404
}
405
406
void BBeBParser::readObject(const unsigned id, const unsigned type)
407
84.6k
{
408
84.6k
  if (m_objectNesting >= BBEB_MAX_OBJECT_NESTING)
409
0
    throw ParserException();
410
84.6k
  const NestingGuard guard(m_objectNesting);
411
412
84.6k
  auto it = m_objectIndex.find(id);
413
84.6k
  if (it == m_objectIndex.end())
414
701
  {
415
701
    EBOOK_DEBUG_MSG(("object with ID %x not found\n", id));
416
701
    throw ParserException();
417
701
  }
418
419
83.9k
  BBeBIndexEntry &entry = it->second;
420
83.9k
  if (entry.reading)
421
24
  {
422
24
    EBOOK_DEBUG_MSG(("object %x is already being read\n", id));
423
24
    throw ParserException();
424
24
  }
425
83.9k
  if (entry.read)
426
65.6k
  {
427
65.6k
    EBOOK_DEBUG_MSG(("object %x has already been read\n", id));
428
65.6k
  }
429
430
83.9k
  m_input->seek((long) entry.offset, librevenge::RVNG_SEEK_SET);
431
432
83.9k
  const unsigned startTag = readU16(m_input);
433
83.9k
  if (TAG_OBJECT_START != startTag)
434
39
  {
435
39
    EBOOK_DEBUG_MSG(("reading object %x, but there is no object at the position\n", id));
436
39
    throw ParserException();
437
39
  }
438
439
83.8k
  const unsigned objectID = readU32(m_input);
440
83.8k
  if (id != objectID)
441
54
  {
442
54
    EBOOK_DEBUG_MSG(("expected object with ID %x, but found %x\n", id, objectID));
443
54
    throw ParserException();
444
54
  }
445
446
83.8k
  const unsigned objectType = readU16(m_input);
447
83.8k
  if ((OBJECT_TYPE_PAGE_TREE > objectType) || (OBJECT_TYPE_TOC < objectType))
448
19
  {
449
19
    EBOOK_DEBUG_MSG(("object is of unknown type %x\n", objectType));
450
19
    throw ParserException();
451
19
  }
452
83.8k
  if ((OBJECT_TYPE_UNSPECIFIED != type) && (type != objectType))
453
20
  {
454
20
    EBOOK_DEBUG_MSG(("expected object of type %x, but found %x\n", type, objectType));
455
20
    throw ParserException();
456
20
  }
457
458
83.7k
  const unsigned char *const data = readNBytes(m_input, entry.size - 10);
459
83.7k
  EBOOKMemoryStream strm(data, entry.size - 10);
460
461
83.7k
  const unsigned endTag = readU16(m_input);
462
83.7k
  if (TAG_OBJECT_END != endTag)
463
15
  {
464
15
    EBOOK_DEBUG_MSG(("no end tag found at the end of the object\n"));
465
15
    throw ParserException();
466
15
  }
467
468
83.7k
  entry.reading = true;
469
470
83.7k
  switch (objectType)
471
83.7k
  {
472
3.08k
  case OBJECT_TYPE_PAGE_TREE :
473
3.08k
    m_pageTree = id;
474
3.08k
    readPageTreeObject(&strm);
475
3.08k
    m_pageTree = 0;
476
3.08k
    break;
477
16.2k
  case OBJECT_TYPE_PAGE :
478
16.2k
    readPageObject(&strm);
479
16.2k
    break;
480
141
  case OBJECT_TYPE_HEADER :
481
141
    readHeaderObject(&strm);
482
141
    break;
483
135
  case OBJECT_TYPE_FOOTER :
484
135
    readFooterObject(&strm);
485
135
    break;
486
5.32k
  case OBJECT_TYPE_PAGE_ATR :
487
5.32k
    readPageAtrObject(&strm, id);
488
5.32k
    break;
489
23.1k
  case OBJECT_TYPE_BLOCK :
490
23.1k
    readBlockObject(&strm, id);
491
23.1k
    break;
492
3.81k
  case OBJECT_TYPE_BLOCK_ATR :
493
3.81k
    readBlockAtrObject(&strm, id);
494
3.81k
    break;
495
6
  case OBJECT_TYPE_MINI_PAGE :
496
6
    readMiniPageObject(&strm);
497
6
    break;
498
2
  case OBJECT_TYPE_BLOCK_LIST :
499
2
    readBlockListObject(&strm);
500
2
    break;
501
13.5k
  case OBJECT_TYPE_TEXT :
502
13.5k
    readTextObject(&strm);
503
13.5k
    break;
504
2.33k
  case OBJECT_TYPE_TEXT_ATR :
505
2.33k
    readTextAtrObject(&strm, id);
506
2.33k
    break;
507
3.74k
  case OBJECT_TYPE_IMAGE :
508
3.74k
    readImageObject(&strm, id);
509
3.74k
    break;
510
182
  case OBJECT_TYPE_CANVAS :
511
182
    readCanvasObject(&strm);
512
182
    break;
513
1.53k
  case OBJECT_TYPE_PARAGRAPH_ATR :
514
1.53k
    readParagraphAtrObject(&strm, id);
515
1.53k
    break;
516
1.98k
  case OBJECT_TYPE_IMAGE_STREAM :
517
1.98k
    readImageStreamObject(&strm, id);
518
1.98k
    break;
519
2
  case OBJECT_TYPE_SoftBookORT :
520
2
    readImportObject(&strm);
521
2
    break;
522
6
  case OBJECT_TYPE_BUTTON :
523
6
    readButtonObject(&strm);
524
6
    break;
525
0
  case OBJECT_TYPE_WINDOW :
526
0
    readWindowObject(&strm);
527
0
    break;
528
8
  case OBJECT_TYPE_POP_UP_WIN :
529
8
    readPopUpWinObject(&strm);
530
8
    break;
531
24
  case OBJECT_TYPE_SOUND :
532
24
    readSoundObject(&strm);
533
24
    break;
534
3
  case OBJECT_TYPE_PLANE_STREAM :
535
3
    readPlaneStreamObject(&strm);
536
3
    break;
537
54
  case OBJECT_TYPE_FONT :
538
54
    readFontObject(&strm);
539
54
    break;
540
6
  case OBJECT_TYPE_OBJECT_INFO :
541
6
    readObjectInfoObject(&strm);
542
6
    break;
543
4.10k
  case OBJECT_TYPE_BOOK_ATR :
544
4.10k
    readBookAtrObject(&strm);
545
4.10k
    break;
546
0
  case OBJECT_TYPE_SSoftBookLE_TEXT :
547
0
    readSimpleTextObject(&strm);
548
0
    break;
549
4.06k
  case OBJECT_TYPE_TOC :
550
4.06k
    readTOCObject(&strm);
551
4.06k
    break;
552
212
  default :
553
212
    EBOOK_DEBUG_MSG(("unhandled object type %x\n", type));
554
212
    break;
555
83.7k
  }
556
557
74.4k
  entry.reading = false;
558
74.4k
  entry.read = true;
559
74.4k
}
560
561
void BBeBParser::readPageTreeObject(librevenge::RVNGInputStream *const object)
562
3.08k
{
563
3.08k
  bool readAnyPage = false;
564
3.08k
  if (TAG_PAGE_LIST == readU16(object))
565
3.05k
  {
566
3.05k
    unsigned count = readU16(object);
567
3.05k
    if (count > getRemainingLength(object) / 4)
568
513
      count = getRemainingLength(object) / 4;
569
3.05k
    readAnyPage = 0 != count;
570
6.73k
    for (unsigned i = 0; i != count; ++i)
571
3.67k
      readObject(readU32(object), OBJECT_TYPE_PAGE);
572
3.05k
  }
573
574
3.08k
  if (!readAnyPage)
575
24
  {
576
24
    EBOOK_DEBUG_MSG(("page tree has no pages\n"));
577
24
    throw ParserException();
578
24
  }
579
3.08k
}
580
581
void BBeBParser::readPageObject(librevenge::RVNGInputStream *const object)
582
16.2k
{
583
16.2k
  unsigned pageAtrID = 0;
584
16.2k
  BBeBAttributes attributes;
585
16.2k
  unsigned streamFlags = 0;
586
16.2k
  unsigned streamSize = 0;
587
16.2k
  std::unique_ptr<EBOOKMemoryStream> strm;
588
589
1.19M
  while (!object->isEnd())
590
1.18M
  {
591
1.18M
    const unsigned tag = readU16(object);
592
1.18M
    switch (tag)
593
1.18M
    {
594
3.95k
    case TAG_LINK :
595
3.95k
      pageAtrID = readU32(object);
596
      // It is possible that pages can share attributes. So avoid
597
      // reading the same Page Atr object twice.
598
3.95k
      if (!isObjectRead(pageAtrID))
599
320
        readObject(pageAtrID, OBJECT_TYPE_PAGE_ATR);
600
3.95k
      break;
601
7.54k
    case TAG_PARENT_PAGE_TREE :
602
7.54k
      if (readU32(object) != m_pageTree)
603
44
      {
604
44
        EBOOK_DEBUG_MSG(("page is not belonging to the current page tree\n"));
605
44
        throw ParserException();
606
44
      }
607
7.50k
      break;
608
9.19k
    case TAG_STREAM_FLAGS :
609
9.19k
      streamFlags = readU16(object);
610
9.19k
      if (streamFlags != 0)
611
10
      {
612
10
        EBOOK_DEBUG_MSG(("page stream with weird flags, giving up\n"));
613
10
        throw ParserException();
614
10
      }
615
9.18k
      break;
616
21.3k
    case TAG_STREAM_SIZE :
617
21.3k
      streamSize = readU32(object);
618
21.3k
      if (streamSize > getRemainingLength(object))
619
2.86k
        streamSize = getRemainingLength(object);
620
21.3k
      break;
621
18.4k
    case TAG_STREAM_START :
622
18.4k
    {
623
18.4k
      const unsigned char *streamData = readNBytes(object, streamSize);
624
18.4k
      strm.reset(new EBOOKMemoryStream(streamData, streamSize));
625
18.4k
      if (TAG_STREAM_END != readU16(object))
626
29
      {
627
29
        EBOOK_DEBUG_MSG(("Stream does not end by Stream End tag\n"));
628
29
        throw ParserException();
629
29
      }
630
18.3k
      break;
631
18.4k
    }
632
1.12M
    default :
633
1.12M
      if (!readAttribute(tag, object, attributes))
634
1.09M
        skipUnhandledTag(tag, object, "Page");
635
1.12M
      break;
636
1.18M
    }
637
1.18M
  }
638
639
16.0k
  if (!strm)
640
20
  {
641
20
    EBOOK_DEBUG_MSG(("No stream data in PageObject\n"));
642
20
    throw ParserException();
643
20
  }
644
645
16.0k
  m_collector.openPage(pageAtrID, attributes);
646
647
316k
  while (!strm->isEnd())
648
300k
  {
649
300k
    const unsigned tag = readU16(strm.get());
650
300k
    if (TAG_LINK == tag)
651
51.6k
      readObject(readU32(strm.get())); // this might be a block or a canvas
652
248k
    else
653
248k
      skipUnhandledTag(tag, strm.get(), "Page Stream");
654
300k
  }
655
656
16.0k
  m_collector.closePage();
657
16.0k
}
658
659
void BBeBParser::readFooterObject(librevenge::RVNGInputStream *const object)
660
135
{
661
  // TODO: implement me
662
135
  (void) object;
663
135
  EBOOK_DEBUG_MSG(("object type Footer is not supported yet\n"));
664
135
}
665
666
void BBeBParser::readHeaderObject(librevenge::RVNGInputStream *const object)
667
141
{
668
  // TODO: implement me
669
141
  (void) object;
670
141
  EBOOK_DEBUG_MSG(("object type Header is not supported yet\n"));
671
141
}
672
673
void BBeBParser::readPageAtrObject(librevenge::RVNGInputStream *const object, const unsigned id)
674
5.32k
{
675
5.32k
  BBeBAttributes attributes;
676
677
1.00M
  while (!object->isEnd())
678
1.00M
  {
679
1.00M
    const unsigned tag = readU16(object);
680
1.00M
    if (!readAttribute(tag, object, attributes))
681
952k
      skipUnhandledTag(tag, object, "Page Atr");
682
1.00M
  }
683
684
5.32k
  m_collector.collectPageAttributes(id, attributes);
685
5.32k
}
686
687
void BBeBParser::readBlockObject(librevenge::RVNGInputStream *const object, const unsigned id)
688
23.1k
{
689
23.1k
  unsigned blockAtrID = 0;
690
23.1k
  BBeBAttributes attributes;
691
23.1k
  unsigned streamFlags = 0;
692
23.1k
  unsigned streamSize = 0;
693
23.1k
  std::unique_ptr<EBOOKMemoryStream> strm;
694
695
447k
  while (!object->isEnd())
696
424k
  {
697
424k
    const unsigned tag = readU16(object);
698
424k
    switch (tag)
699
424k
    {
700
16.9k
    case TAG_LINK :
701
16.9k
      blockAtrID = readU32(object);
702
16.9k
      if (!isObjectRead(blockAtrID))
703
572
        readObject(blockAtrID, OBJECT_TYPE_BLOCK_ATR);
704
16.9k
      break;
705
20.7k
    case TAG_STREAM_FLAGS :
706
20.7k
      streamFlags = readU16(object);
707
20.7k
      break;
708
27.4k
    case TAG_STREAM_SIZE :
709
27.4k
      streamSize = readU32(object);
710
27.4k
      if (streamSize > getRemainingLength(object))
711
2.11k
        streamSize = getRemainingLength(object);
712
27.4k
      break;
713
25.1k
    case TAG_STREAM_START :
714
25.1k
    {
715
25.1k
      const unsigned char *const streamData = readNBytes(object, streamSize);
716
25.1k
      if (0 == streamFlags)
717
23.7k
        strm.reset(new EBOOKMemoryStream(streamData, streamSize));
718
25.1k
      if (TAG_STREAM_END != readU16(object))
719
7
      {
720
7
        EBOOK_DEBUG_MSG(("Stream does not end by Stream End tag\n"));
721
7
        throw ParserException();
722
7
      }
723
25.1k
      break;
724
25.1k
    }
725
334k
    default :
726
334k
      if (!readAttribute(tag, object, attributes))
727
320k
        skipUnhandledTag(tag, object, "Block");
728
334k
      break;
729
424k
    }
730
424k
  }
731
732
23.0k
  if ((0 == streamFlags) && !strm)
733
6
  {
734
6
    EBOOK_DEBUG_MSG(("No stream data in BlockObject\n"));
735
6
    throw ParserException();
736
6
  }
737
738
23.0k
  if (std::binary_search(m_toc.begin(), m_toc.end(), id))
739
299
    attributes.heading = true;
740
741
23.0k
  m_collector.openBlock(blockAtrID, attributes);
742
743
23.0k
  if (bool(strm))
744
23.0k
  {
745
127k
    while (!strm->isEnd())
746
104k
    {
747
104k
      const unsigned tag = readU16(strm.get());
748
104k
      if (TAG_LINK == tag)
749
19.7k
        readObject(readU32(strm.get()));
750
85.1k
      else
751
85.1k
        skipUnhandledTag(tag, strm.get(), "Block Stream");
752
104k
    }
753
23.0k
  }
754
17
  else
755
17
  {
756
17
    EBOOK_DEBUG_MSG(("block stream with weird flags, giving up\n"));
757
17
    throw ParserException();
758
17
  }
759
760
23.0k
  m_collector.closeBlock();
761
23.0k
}
762
763
void BBeBParser::readBlockAtrObject(librevenge::RVNGInputStream *const object, const unsigned id)
764
3.81k
{
765
3.81k
  BBeBAttributes attributes;
766
767
2.38M
  while (!object->isEnd())
768
2.38M
  {
769
2.38M
    const unsigned tag = readU16(object);
770
2.38M
    if (!readAttribute(tag, object, attributes))
771
2.24M
      skipUnhandledTag(tag, object, "Block Atr");
772
2.38M
  }
773
774
3.81k
  m_collector.collectBlockAttributes(id, attributes);
775
3.81k
}
776
777
void BBeBParser::readMiniPageObject(librevenge::RVNGInputStream *const object)
778
6
{
779
  // TODO: implement me
780
6
  (void) object;
781
6
  EBOOK_DEBUG_MSG(("object type Mini Page is not supported yet\n"));
782
6
}
783
784
void BBeBParser::readBlockListObject(librevenge::RVNGInputStream *const object)
785
2
{
786
  // TODO: implement me
787
2
  (void) object;
788
2
  EBOOK_DEBUG_MSG(("object type Block List is not supported yet\n"));
789
2
}
790
791
void BBeBParser::readTextObject(librevenge::RVNGInputStream *const object)
792
13.5k
{
793
13.5k
  unsigned textAtrID = 0;
794
13.5k
  BBeBAttributes attributes;
795
13.5k
  unsigned streamFlags = 0;
796
13.5k
  unsigned streamSize = 0;
797
13.5k
  std::unique_ptr<librevenge::RVNGInputStream> textStrm;
798
13.5k
  unsigned textLength = 0;
799
800
661k
  while (!object->isEnd())
801
647k
  {
802
647k
    const unsigned tag = readU16(object);
803
647k
    switch (tag)
804
647k
    {
805
6.72k
    case TAG_LINK :
806
6.72k
      textAtrID = readU32(object);
807
6.72k
      if (!isObjectRead(textAtrID))
808
385
        readObject(textAtrID, OBJECT_TYPE_TEXT_ATR);
809
6.72k
      break;
810
11.5k
    case TAG_STREAM_FLAGS :
811
11.5k
      streamFlags = readU16(object);
812
11.5k
      break;
813
14.6k
    case TAG_STREAM_SIZE :
814
14.6k
      streamSize = readU32(object);
815
14.6k
      break;
816
12.9k
    case TAG_STREAM_START :
817
12.9k
    {
818
12.9k
      if (streamSize > getRemainingLength(object))
819
31
        streamSize = getRemainingLength(object);
820
12.9k
      if (STREAM_TYPE_BBEB_TAGS_COMPRESSED == streamFlags)
821
10.8k
      {
822
10.8k
        textLength = readU32(object);
823
10.8k
        if (streamSize <= 4)
824
2
        {
825
2
          EBOOK_DEBUG_MSG(("Compressed stream is too short\n"));
826
2
          throw ParserException();
827
2
        }
828
10.8k
        streamSize -= 4;
829
10.8k
      }
830
2.11k
      else
831
2.11k
        textLength = streamSize;
832
12.9k
      const unsigned char *const streamData = readNBytes(object, streamSize);
833
834
      // prepare text stream
835
12.9k
      if (STREAM_TYPE_BBEB_TAGS == streamFlags)
836
2.10k
        textStrm.reset(new EBOOKMemoryStream(streamData, streamSize));
837
10.8k
      else if (STREAM_TYPE_BBEB_TAGS_COMPRESSED == streamFlags)
838
10.8k
      {
839
10.8k
        EBOOKMemoryStream strm(streamData, streamSize);
840
10.8k
        textStrm.reset(new EBOOKZlibStream(&strm));
841
10.8k
      }
842
18
      else
843
18
      {
844
18
        EBOOK_DEBUG_MSG(("Text stream of invalid type\n"));
845
18
        throw ParserException();
846
18
      }
847
848
12.9k
      if (TAG_STREAM_END != readU16(object))
849
13
      {
850
13
        EBOOK_DEBUG_MSG(("Stream does not end by Stream End tag\n"));
851
13
        throw ParserException();
852
13
      }
853
12.9k
    }
854
12.9k
    break;
855
602k
    default :
856
602k
      if (!readAttribute(tag, object, attributes))
857
579k
        skipUnhandledTag(tag, object, "Text");
858
602k
      break;
859
647k
    }
860
647k
  }
861
862
13.4k
  if (0 != textLength)
863
10.6k
  {
864
10.6k
    m_collector.openTextBlock(textAtrID, attributes);
865
866
    // process text, if any
867
10.6k
    BBeBAttributes textAttributes;
868
10.6k
    bool inParagraph = false;
869
870
936k
    while (!textStrm->isEnd())
871
927k
    {
872
927k
      const unsigned tag = readU16(textStrm.get());
873
874
927k
      switch (tag)
875
927k
      {
876
24.4k
      case TAG_BEGIN_P :
877
24.4k
      {
878
        // NOTE: I am not quite sure the argument is an object ID.
879
        // But it makes sense.
880
24.4k
        const unsigned paragraphAtrID = readU32(textStrm.get());
881
24.4k
        if ((0 != paragraphAtrID) && !isObjectRead(paragraphAtrID))
882
93
          readObject(paragraphAtrID, OBJECT_TYPE_PARAGRAPH_ATR);
883
24.4k
        m_collector.openParagraph(paragraphAtrID, attributes);
884
24.4k
        inParagraph = true;
885
24.4k
      }
886
24.4k
      break;
887
3.46k
      case TAG_END_P :
888
3.46k
        if (inParagraph)
889
1.57k
        {
890
1.57k
          m_collector.closeParagraph();
891
1.57k
        }
892
1.89k
        else
893
1.89k
        {
894
1.89k
          EBOOK_DEBUG_MSG(("End P tag encountered, but there is no paragraph opened\n"));
895
1.89k
        }
896
3.46k
        inParagraph = false;
897
3.46k
        break;
898
59.8k
      case TAG_TEXT_SIZE :
899
59.8k
      {
900
59.8k
        const std::string &text = readString(textStrm.get());
901
59.8k
        m_collector.collectText(text, textAttributes);
902
59.8k
      }
903
59.8k
      break;
904
5.36k
      case TAG_EOL :
905
5.36k
        if (inParagraph)
906
2.56k
          m_collector.insertLineBreak();
907
5.36k
        break;
908
3.23k
      case TAG_KOMA_PLOT :
909
3.23k
      {
910
3.23k
        skip(textStrm.get(), 4);
911
3.23k
        const unsigned imageID = readU32(textStrm.get());
912
3.23k
        skip(textStrm.get(), 4);
913
3.23k
        if (!isObjectRead(imageID))
914
124
          readObject(imageID, OBJECT_TYPE_IMAGE);
915
3.23k
        m_collector.insertImage(imageID);
916
3.23k
      }
917
3.23k
      break;
918
830k
      default :
919
830k
        if (!readAttribute(tag, textStrm.get(), textAttributes))
920
799k
          skipUnhandledTag(tag, textStrm.get(), "Text Stream");
921
927k
      }
922
927k
    }
923
924
9.45k
    if (inParagraph)
925
2.98k
    {
926
2.98k
      EBOOK_DEBUG_MSG(("No End P tag encountered\n"));
927
2.98k
      m_collector.closeParagraph();
928
2.98k
    }
929
930
9.45k
    m_collector.closeTextBlock();
931
9.45k
  }
932
13.4k
}
933
934
void BBeBParser::readTextAtrObject(librevenge::RVNGInputStream *const object, const unsigned id)
935
2.33k
{
936
2.33k
  BBeBAttributes attributes;
937
938
119k
  while (!object->isEnd())
939
116k
  {
940
116k
    const unsigned tag = readU16(object);
941
116k
    if (!readAttribute(tag, object, attributes))
942
100k
      skipUnhandledTag(tag, object, "Text Atr");
943
116k
  }
944
945
2.33k
  m_collector.collectTextAttributes(id, attributes);
946
2.33k
}
947
948
void BBeBParser::readImageObject(librevenge::RVNGInputStream *const object, const unsigned id)
949
3.74k
{
950
3.74k
  unsigned width = 0;
951
3.74k
  unsigned height = 0;
952
953
207k
  while (!object->isEnd())
954
203k
  {
955
203k
    const unsigned tag = readU16(object);
956
957
203k
    switch (tag)
958
203k
    {
959
1.27k
    case TAG_IMAGE_CROP_RECT :
960
1.27k
      skip(object, 8);
961
1.27k
      break;
962
2.30k
    case TAG_IMAGE_SIZE :
963
2.30k
    {
964
2.30k
      width = readU16(object);
965
2.30k
      height = readU16(object);
966
2.30k
      break;
967
0
    }
968
2.57k
    case TAG_IMAGE_STREAM :
969
2.57k
    {
970
2.57k
      const unsigned image = readU32(object);
971
2.57k
      if (!isObjectRead(image))
972
127
        readObject(image, OBJECT_TYPE_IMAGE_STREAM);
973
2.57k
      m_collector.collectImage(id, image, width, height);
974
2.57k
      break;
975
0
    }
976
197k
    default :
977
197k
      skipUnhandledTag(tag, object, "Image");
978
197k
      break;
979
203k
    }
980
203k
  }
981
3.74k
}
982
983
void BBeBParser::readCanvasObject(librevenge::RVNGInputStream *const object)
984
182
{
985
  // TODO: implement me
986
182
  (void) object;
987
182
  EBOOK_DEBUG_MSG(("object type Canvas is not supported yet\n"));
988
182
}
989
990
void BBeBParser::readParagraphAtrObject(librevenge::RVNGInputStream *const object, const unsigned id)
991
1.53k
{
992
1.53k
  BBeBAttributes attributes;
993
994
179k
  while (!object->isEnd())
995
178k
  {
996
178k
    const unsigned tag = readU16(object);
997
178k
    if (!readAttribute(tag, object, attributes))
998
162k
      skipUnhandledTag(tag, object, "Paragraph Atr");
999
178k
  }
1000
1001
1.53k
  m_collector.collectParagraphAttributes(id, attributes);
1002
1.53k
}
1003
1004
void BBeBParser::readImageStreamObject(librevenge::RVNGInputStream *const object, const unsigned id)
1005
1.98k
{
1006
1.98k
  unsigned streamType = 0;
1007
1.98k
  unsigned streamSize = 0;
1008
1.98k
  RVNGInputStreamPtr_t image;
1009
1010
482k
  while (!object->isEnd())
1011
480k
  {
1012
480k
    const unsigned tag = readU16(object);
1013
1014
480k
    switch (tag)
1015
480k
    {
1016
2.36k
    case TAG_STREAM_FLAGS :
1017
2.36k
      streamType = readU16(object);
1018
2.36k
      break;
1019
3.37k
    case TAG_STREAM_SIZE :
1020
3.37k
      streamSize = readU16(object);
1021
3.37k
      if (streamSize > getRemainingLength(object))
1022
667
        streamSize = getRemainingLength(object);
1023
3.37k
      break;
1024
2.55k
    case TAG_STREAM_START :
1025
2.55k
    {
1026
2.55k
      const unsigned char *const streamData = readNBytes(object, streamSize);
1027
1028
2.55k
      switch (streamType)
1029
2.55k
      {
1030
1.42k
      case STREAM_TYPE_JPEG :
1031
1.43k
      case STREAM_TYPE_PNG :
1032
1.99k
      case STREAM_TYPE_BMP :
1033
2.54k
      case STREAM_TYPE_GIF :
1034
2.54k
        image.reset(new EBOOKMemoryStream(streamData, streamSize));
1035
2.54k
        break;
1036
5
      default :
1037
5
        EBOOK_DEBUG_MSG(("unknown image type %x\n", streamType));
1038
5
        throw ParserException();
1039
2.55k
      }
1040
1041
2.54k
      if (TAG_STREAM_END != readU16(object))
1042
16
      {
1043
16
        EBOOK_DEBUG_MSG(("Stream does not end by Stream End tag\n"));
1044
16
        throw ParserException();
1045
16
      }
1046
2.53k
      break;
1047
2.54k
    }
1048
472k
    default :
1049
472k
      skipUnhandledTag(tag, object, "Image Stream");
1050
480k
    }
1051
480k
  }
1052
1053
1.95k
  if (!image)
1054
9
  {
1055
9
    EBOOK_DEBUG_MSG(("No stream data in ImageStreamObject\n"));
1056
9
    throw ParserException();
1057
9
  }
1058
1059
1.94k
  m_collector.collectImageData(id, static_cast<BBeBImageType>(streamType), image);
1060
1.94k
}
1061
1062
void BBeBParser::readImportObject(librevenge::RVNGInputStream *const object)
1063
2
{
1064
  // TODO: implement me
1065
2
  (void) object;
1066
2
  EBOOK_DEBUG_MSG(("object type Import is not supported yet\n"));
1067
2
}
1068
1069
void BBeBParser::readButtonObject(librevenge::RVNGInputStream *const object)
1070
6
{
1071
  // TODO: implement me
1072
6
  (void) object;
1073
6
  EBOOK_DEBUG_MSG(("object type Button is not supported yet\n"));
1074
6
}
1075
1076
void BBeBParser::readWindowObject(librevenge::RVNGInputStream *const object)
1077
0
{
1078
  // TODO: implement me
1079
0
  (void) object;
1080
0
  EBOOK_DEBUG_MSG(("object type Window is not supported yet\n"));
1081
0
}
1082
1083
void BBeBParser::readPopUpWinObject(librevenge::RVNGInputStream *const object)
1084
8
{
1085
  // TODO: implement me
1086
8
  (void) object;
1087
8
  EBOOK_DEBUG_MSG(("object type Pop Up Win is not supported yet\n"));
1088
8
}
1089
1090
void BBeBParser::readSoundObject(librevenge::RVNGInputStream *const object)
1091
24
{
1092
  // TODO: implement me
1093
24
  (void) object;
1094
24
  EBOOK_DEBUG_MSG(("object type Sound is not supported yet\n"));
1095
24
}
1096
1097
void BBeBParser::readPlaneStreamObject(librevenge::RVNGInputStream *const object)
1098
3
{
1099
  // TODO: implement me
1100
3
  (void) object;
1101
3
  EBOOK_DEBUG_MSG(("object type Plane Stream is not supported yet\n"));
1102
3
}
1103
1104
void BBeBParser::readFontObject(librevenge::RVNGInputStream *const object)
1105
54
{
1106
  // TODO: implement me
1107
54
  (void) object;
1108
54
  EBOOK_DEBUG_MSG(("object type Font is not supported yet\n"));
1109
54
}
1110
1111
void BBeBParser::readObjectInfoObject(librevenge::RVNGInputStream *const object)
1112
6
{
1113
  // TODO: implement me
1114
6
  (void) object;
1115
6
  EBOOK_DEBUG_MSG(("object type Object Info is not supported yet\n"));
1116
6
}
1117
1118
void BBeBParser::readBookAtrObject(librevenge::RVNGInputStream *const object)
1119
4.10k
{
1120
4.10k
  BBeBAttributes attributes;
1121
4.10k
  unsigned pageTree = 0;
1122
1123
388k
  while (!object->isEnd())
1124
384k
  {
1125
384k
    const unsigned tag = readU16(object);
1126
384k
    switch (tag)
1127
384k
    {
1128
3.52k
    case TAG_CHILD_PAGE_TREE :
1129
3.52k
      pageTree = readU32(object);
1130
3.52k
      break;
1131
380k
    default :
1132
380k
      if (!readAttribute(tag, object, attributes))
1133
364k
        skipUnhandledTag(tag, object, "Book Atr");
1134
384k
    }
1135
384k
  }
1136
1137
3.48k
  m_collector.collectBookAttributes(attributes);
1138
1139
3.48k
  if (0 == pageTree)
1140
339
  {
1141
339
    EBOOK_DEBUG_MSG(("the book has no page tree"));
1142
339
    throw ParserException();
1143
339
  }
1144
3.14k
  readObject(pageTree, OBJECT_TYPE_PAGE_TREE);
1145
3.14k
}
1146
1147
void BBeBParser::readSimpleTextObject(librevenge::RVNGInputStream *const object)
1148
0
{
1149
  // TODO: implement me
1150
0
  (void) object;
1151
0
  EBOOK_DEBUG_MSG(("object type Simple Text is not supported yet\n"));
1152
0
}
1153
1154
void BBeBParser::readTOCObject(librevenge::RVNGInputStream *const object)
1155
4.06k
{
1156
4.06k
  unsigned streamFlags = 0;
1157
4.06k
  unsigned streamSize = 0;
1158
4.06k
  std::unique_ptr<EBOOKMemoryStream> data;
1159
1160
836k
  while (!object->isEnd())
1161
833k
  {
1162
833k
    const unsigned tag = readU16(object);
1163
833k
    switch (tag)
1164
833k
    {
1165
6.11k
    case TAG_STREAM_FLAGS :
1166
6.11k
      streamFlags = readU16(object);
1167
6.11k
      if (STREAM_TYPE_BBEB_TOC != streamFlags)
1168
3.32k
      {
1169
3.32k
        EBOOK_DEBUG_MSG(("unexpected ToC stream type %x\n", streamFlags));
1170
3.32k
      }
1171
6.11k
      break;
1172
7.31k
    case TAG_STREAM_SIZE :
1173
7.31k
      streamSize = readU32(object);
1174
7.31k
      if (streamSize > getRemainingLength(object))
1175
1.69k
        streamSize = getRemainingLength(object);
1176
7.31k
      break;
1177
7.37k
    case TAG_STREAM_START :
1178
7.37k
      if (STREAM_TYPE_BBEB_TOC == streamFlags)
1179
2.99k
      {
1180
2.99k
        const unsigned char *const streamData = readNBytes(object, streamSize);
1181
2.99k
        data.reset(new EBOOKMemoryStream(streamData, streamSize));
1182
2.99k
        if (TAG_STREAM_END != readU16(object))
1183
8
        {
1184
8
          EBOOK_DEBUG_MSG(("Stream does not end by Stream End tag\n"));
1185
8
          throw ParserException();
1186
8
        }
1187
2.99k
      }
1188
4.38k
      else
1189
4.38k
      {
1190
4.38k
        skip(object, streamSize);
1191
4.38k
      }
1192
7.36k
      break;
1193
812k
    default :
1194
812k
      skipUnhandledTag(tag, object, "ToC");
1195
812k
      break;
1196
833k
    }
1197
833k
  }
1198
1199
3.82k
  if (bool(data))
1200
2.65k
    readToCStream(data.get());
1201
3.82k
}
1202
1203
void BBeBParser::readToCStream(librevenge::RVNGInputStream *const input)
1204
2.65k
{
1205
2.65k
  unsigned count = readU32(input);
1206
2.65k
  if (count > getRemainingLength(input) / 4)
1207
267
    count = getRemainingLength(input) / 4;
1208
2.65k
  std::vector<unsigned> offsets;
1209
1210
2.65k
  offsets.reserve(count);
1211
29.7k
  for (unsigned i = 0; count != i; ++i)
1212
27.1k
    offsets.push_back(readU32(input));
1213
1214
2.65k
  const long start = input->tell();
1215
2.65k
  m_toc.reserve(count);
1216
28.4k
  for (std::vector<unsigned>::const_iterator it = offsets.begin(); offsets.end() != it; ++it)
1217
25.8k
  {
1218
25.8k
    seek(input, (unsigned long)(start + int(*it) + 4));
1219
25.8k
    const unsigned oid = readU32(input);
1220
25.8k
    if (m_objectIndex.end() != m_objectIndex.find(oid))
1221
17.6k
    {
1222
17.6k
      m_toc.push_back(oid);
1223
17.6k
    }
1224
8.14k
    else
1225
8.14k
    {
1226
8.14k
      EBOOK_DEBUG_MSG(("ToC refers to an unexistent block id %x\n", oid));
1227
8.14k
    }
1228
25.8k
  }
1229
1230
2.65k
  std::sort(m_toc.begin(), m_toc.end());
1231
2.65k
}
1232
1233
bool BBeBParser::readAttribute(const unsigned tag, librevenge::RVNGInputStream *const input, BBeBAttributes &attributes)
1234
6.94M
{
1235
6.94M
  bool handled = true;
1236
1237
6.94M
  switch (tag)
1238
6.94M
  {
1239
15.0k
  case TAG_FONT_SIZE :
1240
15.0k
    attributes.fontSize = readU16(input);
1241
15.0k
    break;
1242
12.3k
  case TAG_FONT_WIDTH :
1243
12.3k
    attributes.fontWidth = readU16(input);
1244
12.3k
    break;
1245
14.5k
  case TAG_FONT_ESCAPEMENT :
1246
14.5k
    attributes.fontEscapement = readU16(input);
1247
14.5k
    break;
1248
13.0k
  case TAG_FONT_ORIENTATION :
1249
13.0k
    attributes.fontOrientation = readU16(input);
1250
13.0k
    break;
1251
12.1k
  case TAG_FONT_WEIGHT :
1252
12.1k
    attributes.fontWeight = readU16(input);
1253
12.1k
    break;
1254
15.9k
  case TAG_FONT_FACENAME :
1255
15.9k
    attributes.fontFacename = readString(input);
1256
15.9k
    break;
1257
10.6k
  case TAG_TEXT_COLOR :
1258
10.6k
    attributes.textColor = BBeBColor(readU32(input));
1259
10.6k
    break;
1260
13.9k
  case TAG_TEXT_BG_COLOR :
1261
13.9k
    attributes.textBgColor = BBeBColor(readU32(input));
1262
13.9k
    break;
1263
15.6k
  case TAG_WORD_SPACE :
1264
15.6k
    attributes.wordSpace = readU16(input);
1265
15.6k
    break;
1266
10.5k
  case TAG_LETTER_SPACE :
1267
10.5k
    attributes.letterSpace = readU16(input);
1268
10.5k
    break;
1269
17.3k
  case TAG_BASE_LINE_SKIP :
1270
17.3k
    attributes.baseLineSkip = readU16(input);
1271
17.3k
    break;
1272
13.7k
  case TAG_LINE_SPACE :
1273
13.7k
    attributes.lineSpace = readU16(input);
1274
13.7k
    break;
1275
11.3k
  case TAG_PAR_INDENT :
1276
11.3k
    attributes.parIndent = readU16(input);
1277
11.3k
    break;
1278
10.6k
  case TAG_PAR_SKIP :
1279
10.6k
    attributes.parSkip = readU16(input);
1280
10.6k
    break;
1281
1.26k
  case TAG_PAGE_HEIGHT :
1282
19.4k
  case TAG_BLOCK_HEIGHT :
1283
21.9k
  case TAG_MINI_PAGE_HEIGHT :
1284
23.6k
  case TAG_CANVAS_HEIGHT :
1285
23.6k
    attributes.height = readU16(input);
1286
23.6k
    break;
1287
4.47k
  case TAG_PAGE_WIDTH :
1288
17.3k
  case TAG_BLOCK_WIDTH :
1289
18.2k
  case TAG_MINI_PAGE_WIDTH :
1290
19.6k
  case TAG_CANVAS_WIDTH :
1291
19.6k
    attributes.width = readU16(input);
1292
19.6k
    break;
1293
2.93k
  case TAG_LOCATION_X :
1294
2.93k
    attributes.locationX = readU16(input);
1295
2.93k
    break;
1296
3.22k
  case TAG_LOCATION_Y :
1297
3.22k
    attributes.locationY = readU16(input);
1298
3.22k
    break;
1299
2.68k
  case TAG_BEGIN_ITALIC :
1300
2.68k
    attributes.italic = true;
1301
2.68k
    break;
1302
4.23k
  case TAG_END_ITALIC :
1303
4.23k
    attributes.italic = false;
1304
4.23k
    break;
1305
1.59k
  case TAG_BEGIN_SUP :
1306
1.59k
    attributes.sup = true;
1307
1.59k
    break;
1308
2.14k
  case TAG_END_SUP :
1309
2.14k
    attributes.sup = false;
1310
2.14k
    break;
1311
2.53k
  case TAG_BEGIN_SUB :
1312
2.53k
    attributes.sub = true;
1313
2.53k
    break;
1314
4.07k
  case TAG_END_SUB :
1315
4.07k
    attributes.sub = false;
1316
4.07k
    break;
1317
14.5k
  case TAG_BEGIN_EMP_LINE :
1318
14.5k
    attributes.emptyLine = BBeBEmptyLine();
1319
14.5k
    break;
1320
7.77k
  case TAG_END_EMP_LINE :
1321
7.77k
    attributes.emptyLine.reset();
1322
7.77k
    break;
1323
4.10k
  case TAG_EMPTY_LINE_POSITION :
1324
4.10k
  {
1325
4.10k
    const unsigned position = readU16(input);
1326
4.10k
    if (attributes.emptyLine)
1327
2.35k
    {
1328
2.35k
      switch (position)
1329
2.35k
      {
1330
491
      case 0x1 :
1331
491
        get(attributes.emptyLine).m_position = BBEB_EMPTY_LINE_POSITION_BEFORE;
1332
491
        break;
1333
1.08k
      case 0x2 :
1334
1.08k
        get(attributes.emptyLine).m_position = BBEB_EMPTY_LINE_POSITION_AFTER;
1335
1.08k
        break;
1336
781
      default :
1337
781
        EBOOK_DEBUG_MSG(("unkown Empty Line Position: %u\n", position));
1338
781
        break;
1339
2.35k
      }
1340
2.35k
    }
1341
1.74k
    else
1342
1.74k
    {
1343
1.74k
      EBOOK_DEBUG_MSG(("Empty Line Position without Begin Emp Line\n"));
1344
1.74k
    }
1345
4.10k
    break;
1346
4.10k
  }
1347
8.26k
  case TAG_EMPTY_LINE_MODE :
1348
8.26k
  {
1349
8.26k
    const unsigned mode = readU16(input);
1350
8.26k
    if (attributes.emptyLine)
1351
4.42k
    {
1352
4.42k
      switch (mode)
1353
4.42k
      {
1354
342
      case 0x0 :
1355
342
        get(attributes.emptyLine).m_mode = BBEB_EMPTY_LINE_MODE_NONE;
1356
342
        break;
1357
1.22k
      case 0x10 :
1358
1.22k
        get(attributes.emptyLine).m_mode = BBEB_EMPTY_LINE_MODE_SOLID;
1359
1.22k
        break;
1360
408
      case 0x20 :
1361
408
        get(attributes.emptyLine).m_mode = BBEB_EMPTY_LINE_MODE_DASHED;
1362
408
        break;
1363
333
      case 0x30 :
1364
333
        get(attributes.emptyLine).m_mode = BBEB_EMPTY_LINE_MODE_DOUBLE;
1365
333
        break;
1366
461
      case 0x40 :
1367
461
        get(attributes.emptyLine).m_mode = BBEB_EMPTY_LINE_MODE_DOTTED;
1368
461
        break;
1369
1.65k
      default :
1370
1.65k
        EBOOK_DEBUG_MSG(("unkown Empty Line Mode: %u\n", mode));
1371
1.65k
        break;
1372
4.42k
      }
1373
4.42k
    }
1374
3.84k
    else
1375
3.84k
    {
1376
3.84k
      EBOOK_DEBUG_MSG(("Empty Line Mode without Begin Emp Line\n"));
1377
3.84k
    }
1378
8.26k
    break;
1379
8.26k
  }
1380
11.3k
  case TAG_ALIGN :
1381
11.3k
  {
1382
11.3k
    const unsigned align = readU16(input);
1383
11.3k
    switch (align)
1384
11.3k
    {
1385
5.01k
    case 0x1 :
1386
5.01k
      attributes.align = BBEB_ALIGN_START;
1387
5.01k
      break;
1388
1.16k
    case 0x4 :
1389
1.16k
      attributes.align = BBEB_ALIGN_CENTER;
1390
1.16k
      break;
1391
1.54k
    case 0x8 :
1392
1.54k
      attributes.align = BBEB_ALIGN_END;
1393
1.54k
      break;
1394
3.66k
    default :
1395
3.66k
      EBOOK_DEBUG_MSG(("unkown Align: %u\n", align));
1396
3.66k
      break;
1397
11.3k
    }
1398
11.3k
    break;
1399
11.3k
  }
1400
15.4k
  case TAG_TOP_SKIP :
1401
15.4k
    attributes.topSkip = readU16(input);
1402
15.4k
    break;
1403
4.15k
  case TAG_TOP_MARGIN :
1404
4.15k
    attributes.topMargin = readU16(input);
1405
4.15k
    break;
1406
3.53k
  case TAG_ODD_SIDE_MARGIN :
1407
3.53k
    attributes.oddSideMargin = readU16(input);
1408
3.53k
    break;
1409
4.40k
  case TAG_EVEN_SIDE_MARGIN :
1410
4.40k
    attributes.evenSideMargin = readU16(input);
1411
4.40k
    break;
1412
6.62M
  default :
1413
6.62M
    handled = false;
1414
6.62M
    break;
1415
6.94M
  }
1416
1417
6.94M
  return handled;
1418
6.94M
}
1419
1420
void BBeBParser::skipUnhandledTag(const unsigned tag, librevenge::RVNGInputStream *const input, const char *const objectType)
1421
8.43M
{
1422
8.43M
  switch (tag)
1423
8.43M
  {
1424
76.1k
  case TAG_OBJECT_END :
1425
76.1k
    EBOOK_DEBUG_MSG(("Object End tag should never appear\n"));
1426
90.6k
  case TAG_STREAM_START :
1427
135k
  case TAG_STREAM_END :
1428
135k
  case TAG_F54D :
1429
135k
  case TAG_F562 :
1430
136k
  case TAG_F563 :
1431
137k
  case TAG_F564 :
1432
139k
  case TAG_F565 :
1433
142k
  case TAG_F566 :
1434
142k
  case TAG_F567 :
1435
143k
  case TAG_F568 :
1436
144k
  case TAG_F569 :
1437
145k
  case TAG_F56A :
1438
147k
  case TAG_F56B :
1439
147k
  case TAG_F56E :
1440
147k
  case TAG_F571 :
1441
148k
  case TAG_F572 :
1442
148k
  case TAG_BEGIN_ITALIC :
1443
149k
  case TAG_END_ITALIC :
1444
149k
  case TAG_END_P :
1445
149k
  case TAG_KOMA_EMP_DOT_CHAR :
1446
149k
  case TAG_END_BUTTON :
1447
149k
  case TAG_BEGIN_RUBY :
1448
151k
  case TAG_END_RUBY :
1449
151k
  case TAG_BEGIN_RUBY_BASE :
1450
151k
  case TAG_END_RUBY_BASE :
1451
152k
  case TAG_BEGIN_RUBY_TEXT :
1452
152k
  case TAG_END_RUBY_TEXT :
1453
152k
  case TAG_KOMA_YOKOMOJI :
1454
152k
  case TAG_F5B2 :
1455
152k
  case TAG_BEGIN_TATE :
1456
153k
  case TAG_END_TATE :
1457
153k
  case TAG_BEGIN_NEKASE :
1458
153k
  case TAG_END_NEKASE :
1459
153k
  case TAG_BEGIN_SUP :
1460
153k
  case TAG_END_SUP :
1461
153k
  case TAG_BEGIN_SUB :
1462
154k
  case TAG_END_SUB :
1463
154k
  case TAG_F5BB :
1464
154k
  case TAG_F5BC :
1465
154k
  case TAG_F5BD :
1466
155k
  case TAG_F5BE :
1467
159k
  case TAG_BEGIN_EMP_LINE :
1468
162k
  case TAG_END_EMP_LINE :
1469
162k
  case TAG_END_DRAW_CHAR :
1470
162k
  case TAG_F5C7 :
1471
162k
  case TAG_F5C9 :
1472
162k
  case TAG_EOL :
1473
162k
  case TAG_SOUND_STOP :
1474
    // no arguments
1475
162k
    break;
1476
1477
2.38k
  case TAG_F50E :
1478
3.82k
  case TAG_FONT_SIZE :
1479
5.97k
  case TAG_FONT_WIDTH :
1480
9.59k
  case TAG_FONT_ESCAPEMENT :
1481
11.3k
  case TAG_FONT_ORIENTATION :
1482
14.4k
  case TAG_FONT_WEIGHT :
1483
15.6k
  case TAG_WORD_SPACE :
1484
17.1k
  case TAG_LETTER_SPACE :
1485
18.8k
  case TAG_BASE_LINE_SKIP :
1486
20.4k
  case TAG_LINE_SPACE :
1487
21.4k
  case TAG_PAR_INDENT :
1488
22.5k
  case TAG_PAR_SKIP :
1489
24.3k
  case TAG_TOP_MARGIN :
1490
31.4k
  case TAG_F522 :
1491
34.6k
  case TAG_F523 :
1492
35.7k
  case TAG_ODD_SIDE_MARGIN :
1493
37.4k
  case TAG_PAGE_HEIGHT :
1494
39.4k
  case TAG_PAGE_WIDTH :
1495
41.3k
  case TAG_F527 :
1496
47.1k
  case TAG_F528 :
1497
53.2k
  case TAG_F52A :
1498
57.9k
  case TAG_F52B :
1499
59.0k
  case TAG_EVEN_SIDE_MARGIN :
1500
80.0k
  case TAG_F52E :
1501
82.2k
  case TAG_BLOCK_WIDTH :
1502
84.3k
  case TAG_BLOCK_HEIGHT :
1503
103k
  case TAG_BLOCK_RULE :
1504
125k
  case TAG_F535 :
1505
144k
  case TAG_F536 :
1506
145k
  case TAG_TOP_SKIP :
1507
163k
  case TAG_F539 :
1508
183k
  case TAG_F53A :
1509
184k
  case TAG_ALIGN :
1510
199k
  case TAG_F53D :
1511
211k
  case TAG_F53E :
1512
211k
  case TAG_MINI_PAGE_HEIGHT :
1513
214k
  case TAG_MINI_PAGE_WIDTH :
1514
215k
  case TAG_LOCATION_Y :
1515
216k
  case TAG_LOCATION_X :
1516
218k
  case TAG_F548 :
1517
219k
  case TAG_CANVAS_WIDTH :
1518
220k
  case TAG_CANVAS_HEIGHT :
1519
233k
  case TAG_STREAM_FLAGS :
1520
234k
  case TAG_F557 :
1521
235k
  case TAG_F558 :
1522
240k
  case TAG_F55E :
1523
243k
  case TAG_F561 :
1524
245k
  case TAG_RUBY_ALIGN :
1525
247k
  case TAG_RUBY_OVERHANG :
1526
249k
  case TAG_EMPTY_DOTS_POSITION :
1527
251k
  case TAG_EMPTY_LINE_POSITION :
1528
254k
  case TAG_EMPTY_LINE_MODE :
1529
255k
  case TAG_BEGIN_DRAW_CHAR :
1530
258k
  case TAG_F5C5 :
1531
259k
  case TAG_F5C6 :
1532
262k
  case TAG_KOMA_AUTO_SPACING :
1533
264k
  case TAG_SPACE :
1534
267k
  case TAG_TEXT_SIZE :
1535
268k
  case TAG_WAIT :
1536
274k
  case TAG_F5DA :
1537
276k
  case TAG_F5DB :
1538
280k
  case TAG_F5DC :
1539
291k
  case TAG_CHAR_SPACE :
1540
300k
  case TAG_LINE_WIDTH :
1541
303k
  case TAG_LINE_MODE :
1542
303k
    skip(input, 2);
1543
303k
    break;
1544
1545
6.40k
  case TAG_OBJECT_INFO_LINK :
1546
69.3k
  case TAG_LINK :
1547
84.4k
  case TAG_STREAM_SIZE :
1548
90.0k
  case TAG_F507 :
1549
93.1k
  case TAG_F508 :
1550
96.2k
  case TAG_F509 :
1551
98.5k
  case TAG_F50A :
1552
100k
  case TAG_TEXT_COLOR :
1553
102k
  case TAG_TEXT_BG_COLOR :
1554
103k
  case TAG_F52D :
1555
121k
  case TAG_F534 :
1556
138k
  case TAG_F537 :
1557
141k
  case TAG_F544 :
1558
143k
  case TAG_F545 :
1559
147k
  case TAG_IMAGE_SIZE :
1560
149k
  case TAG_IMAGE_STREAM :
1561
151k
  case TAG_F553 :
1562
156k
  case TAG_VIEW_POINT :
1563
162k
  case TAG_CHILD_PAGE_TREE :
1564
167k
  case TAG_PARENT_PAGE_TREE :
1565
167k
  case TAG_BEGIN_P :
1566
169k
  case TAG_BEGIN_BUTTON :
1567
171k
  case TAG_BOOK_FONT :
1568
181k
  case TAG_LINE_COLOR :
1569
184k
  case TAG_FILL_COLOR :
1570
208k
  case TAG_MOVE_TO :
1571
210k
  case TAG_LINE_TO :
1572
213k
  case TAG_DRAW_BOX :
1573
215k
  case TAG_DRAW_ELLIPSE :
1574
215k
    skip(input, 4);
1575
215k
    break;
1576
1577
103k
  case TAG_OBJECT_START :
1578
103k
    EBOOK_DEBUG_MSG(("Object Start tag should never appear\n"));
1579
  // fall-through intended
1580
123k
  case TAG_F529 :
1581
125k
  case TAG_F5F9 :
1582
125k
    skip(input, 6);
1583
125k
    break;
1584
1585
1.15k
  case TAG_PUT_SOUND :
1586
2.01k
  case TAG_IMAGE_CROP_RECT :
1587
5.32k
  case TAG_JUMP_TO :
1588
7.31k
  case TAG_KOMA_PLOT_TEXT :
1589
7.31k
    skip(input, 8);
1590
7.31k
    break;
1591
1592
5.83k
  case TAG_RULED_LINE :
1593
5.83k
    skip(input, 10);
1594
5.83k
    break;
1595
1596
836
  case TAG_F54E :
1597
2.25k
  case TAG_KOMA_PLOT :
1598
2.25k
    skip(input, 12);
1599
2.25k
    break;
1600
1601
1.94k
  case TAG_MOVE_OBJ :
1602
1.94k
    skip(input, 14);
1603
1.94k
    break;
1604
1605
2.75k
  case TAG_CONTAINED_OBJECTS_LIST :
1606
13.1k
  case TAG_PAGE_LIST :
1607
13.1k
  {
1608
13.1k
    unsigned count = readU16(input);
1609
13.1k
    skip(input, count * 4);
1610
13.1k
  }
1611
13.1k
  break;
1612
3.95k
  case TAG_F50D :
1613
4.72k
  case TAG_FONT_FACENAME :
1614
5.47k
  case TAG_F555 :
1615
6.00k
  case TAG_F556 :
1616
6.74k
  case TAG_FONT_FILE_NAME :
1617
7.74k
  case TAG_F55A :
1618
9.23k
  case TAG_FONT_FACE_NAME :
1619
10.3k
  case TAG_F56D :
1620
12.2k
  case TAG_EMPTY_DOTS_CODE :
1621
13.2k
  case TAG_KOMA_GAIJI :
1622
14.1k
  case TAG_F5CB :
1623
14.1k
  {
1624
    // try to find the next tag
1625
14.1k
    unsigned n = 1;
1626
9.12M
    while (!input->isEnd() && ((0xf5 != readU8(input)) || (2 <= n)))
1627
9.10M
      ++n;
1628
14.1k
    if (!input->isEnd())
1629
4.18k
      input->seek(-2, librevenge::RVNG_SEEK_CUR);
1630
14.1k
  }
1631
14.1k
  break;
1632
7.58M
  default :
1633
7.58M
    EBOOK_DEBUG_MSG(("unknown tag %x\n", tag));
1634
7.58M
    return;
1635
8.43M
  }
1636
1637
851k
  (void) objectType;
1638
851k
  EBOOK_DEBUG_MSG(("skipping unhandled tag %x at %s object\n", tag, objectType));
1639
851k
}
1640
1641
bool BBeBParser::isObjectRead(const unsigned id) const
1642
33.8k
{
1643
33.8k
  const ObjectIndex_t::const_iterator it = m_objectIndex.find(id);
1644
33.8k
  return (m_objectIndex.end() != it) && it->second.read;
1645
33.8k
}
1646
1647
} // namespace libebook
1648
1649
/* vim:set shiftwidth=2 softtabstop=2 expandtab: */