1 /*
2 
3 Copyright 2014 S. Razi Alavizadeh
4 Copyright 2018 Marshall Banana
5 Copyright 2013-2014, 2018 Adam Reichold
6 Copyright 2013 Alexander Volkov
7 
8 This file is part of qpdfview.
9 
10 The implementation is based on KDjVu by Pino Toscano.
11 
12 qpdfview is free software: you can redistribute it and/or modify
13 it under the terms of the GNU General Public License as published by
14 the Free Software Foundation, either version 2 of the License, or
15 (at your option) any later version.
16 
17 qpdfview is distributed in the hope that it will be useful,
18 but WITHOUT ANY WARRANTY; without even the implied warranty of
19 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
20 GNU General Public License for more details.
21 
22 You should have received a copy of the GNU General Public License
23 along with qpdfview.  If not, see <http://www.gnu.org/licenses/>.
24 
25 */
26 
27 #include "djvumodel.h"
28 
29 #include <cstdio>
30 
31 #include <QFile>
32 #include <QPainterPath>
33 #include <qmath.h>
34 
35 #if defined(Q_OS_WIN) && defined(DJVU_STATIC)
36 
37 #define DDJVUAPI /**/
38 #define MINILISPAPI /**/
39 
40 #endif // Q_OS_WIN DJVU_STATIC
41 
42 #include <libdjvu/ddjvuapi.h>
43 #include <libdjvu/miniexp.h>
44 
45 #define LOCK_PAGE QMutexLocker mutexLocker(&m_parent->m_mutex);
46 #define LOCK_DOCUMENT QMutexLocker mutexLocker(&m_mutex);
47 
48 #if DDJVUAPI_VERSION < 23
49 
50 #define LOCK_PAGE_GLOBAL QMutexLocker globalMutexLocker(m_parent->m_globalMutex);
51 #define LOCK_DOCUMENT_GLOBAL QMutexLocker globalMutexLocker(m_globalMutex);
52 
53 #else
54 
55 #define LOCK_PAGE_GLOBAL
56 #define LOCK_DOCUMENT_GLOBAL
57 
58 #endif // DDJVUAPI_VERSION
59 
60 namespace
61 {
62 
63 using namespace qpdfview;
64 using namespace qpdfview::Model;
65 
miniexp_cadddr(miniexp_t exp)66 inline miniexp_t miniexp_cadddr(miniexp_t exp)
67 {
68     return miniexp_cadr(miniexp_cddr(exp));
69 }
70 
miniexp_caddddr(miniexp_t exp)71 inline miniexp_t miniexp_caddddr(miniexp_t exp)
72 {
73     return miniexp_caddr(miniexp_cddr(exp));
74 }
75 
skip(miniexp_t exp,int offset)76 inline miniexp_t skip(miniexp_t exp, int offset)
77 {
78     while(offset-- > 0)
79     {
80         exp = miniexp_cdr(exp);
81     }
82 
83     return exp;
84 }
85 
clearMessageQueue(ddjvu_context_t * context,bool wait)86 void clearMessageQueue(ddjvu_context_t* context, bool wait)
87 {
88     if(wait)
89     {
90         ddjvu_message_wait(context);
91     }
92 
93     while(true)
94     {
95         if(ddjvu_message_peek(context) != 0)
96         {
97             ddjvu_message_pop(context);
98         }
99         else
100         {
101             break;
102         }
103     }
104 }
105 
waitForMessageTag(ddjvu_context_t * context,ddjvu_message_tag_t tag)106 void waitForMessageTag(ddjvu_context_t* context, ddjvu_message_tag_t tag)
107 {
108     ddjvu_message_wait(context);
109 
110     while(true)
111     {
112         ddjvu_message_t* message = ddjvu_message_peek(context);
113 
114         if(message != 0)
115         {
116             if(message->m_any.tag == tag)
117             {
118                 break;
119             }
120 
121             ddjvu_message_pop(context);
122         }
123         else
124         {
125             break;
126         }
127     }
128 }
129 
loadLinkBoundary(const QString & type,miniexp_t boundaryExp,QSizeF size)130 QPainterPath loadLinkBoundary(const QString& type, miniexp_t boundaryExp, QSizeF size)
131 {
132     QPainterPath boundary;
133 
134     const int count = miniexp_length(boundaryExp);
135 
136     if(count == 4 && (type == QLatin1String("rect") || type == QLatin1String("oval")))
137     {
138         QPoint p(miniexp_to_int(miniexp_car(boundaryExp)), miniexp_to_int(miniexp_cadr(boundaryExp)));
139         QSize s(miniexp_to_int(miniexp_caddr(boundaryExp)), miniexp_to_int(miniexp_cadddr(boundaryExp)));
140 
141         p.setY(size.height() - s.height() - p.y());
142 
143         const QRectF r(p, s);
144 
145         if(type == QLatin1String("rect"))
146         {
147             boundary.addRect(r);
148         }
149         else
150         {
151             boundary.addEllipse(r);
152         }
153     }
154     else if(count > 0 && count % 2 == 0 && type == QLatin1String("poly"))
155     {
156         QPolygon polygon;
157 
158         for(int index = 0; index < count; index += 2)
159         {
160             QPoint p(miniexp_to_int(miniexp_nth(index, boundaryExp)), miniexp_to_int(miniexp_nth(index + 1, boundaryExp)));
161 
162             p.setY(size.height() - p.y());
163 
164             polygon << p;
165         }
166 
167         boundary.addPolygon(polygon);
168     }
169 
170     return QTransform::fromScale(1.0 / size.width(), 1.0 / size.height()).map(boundary);
171 }
172 
loadLinkTarget(const QPainterPath & boundary,miniexp_t targetExp,int index,const QHash<QString,int> & pageByName)173 Link* loadLinkTarget(const QPainterPath& boundary, miniexp_t targetExp, int index, const QHash< QString, int >& pageByName)
174 {
175     QString target;
176 
177     if(miniexp_stringp(targetExp))
178     {
179         target = QString::fromUtf8(miniexp_to_str(targetExp));
180     }
181     else if(miniexp_length(targetExp) == 3 && qstrcmp(miniexp_to_name(miniexp_car(targetExp)), "url") == 0)
182     {
183         target = QString::fromUtf8(miniexp_to_str(miniexp_cadr(targetExp)));
184     }
185 
186     if(target.isEmpty())
187     {
188         return 0;
189     }
190 
191     if(target.at(0) == QLatin1Char('#'))
192     {
193         target.remove(0, 1);
194 
195         bool ok = false;
196         int targetPage = target.toInt(&ok);
197 
198         if(!ok)
199         {
200             const int page = pageByName.value(target);
201 
202             if(page != 0)
203             {
204                 targetPage = page;
205             }
206             else
207             {
208                 return 0;
209             }
210         }
211         else
212         {
213             if(target.at(0) == QLatin1Char('+') || target.at(0) == QLatin1Char('-'))
214             {
215                 targetPage += index + 1;
216             }
217         }
218 
219         return new Link(boundary, targetPage);
220     }
221     else
222     {
223         return new Link(boundary, target);
224     }
225 }
226 
loadLinks(miniexp_t linkExp,QSizeF size,int index,const QHash<QString,int> & pageByName)227 QList< Link* > loadLinks(miniexp_t linkExp, QSizeF size, int index, const QHash< QString, int >& pageByName)
228 {
229     QList< Link* > links;
230 
231     for(miniexp_t linkItem = miniexp_nil; miniexp_consp(linkExp); linkExp = miniexp_cdr(linkExp))
232     {
233         linkItem = miniexp_car(linkExp);
234 
235         if(miniexp_length(linkItem) < 4 || qstrcmp(miniexp_to_name(miniexp_car(linkItem)), "maparea") != 0)
236         {
237             continue;
238         }
239 
240         miniexp_t targetExp = miniexp_cadr(linkItem);
241         miniexp_t boundaryExp = miniexp_cadddr(linkItem);
242 
243         if(!miniexp_symbolp(miniexp_car(boundaryExp)))
244         {
245             continue;
246         }
247 
248         const QString type = QString::fromUtf8(miniexp_to_name(miniexp_car(boundaryExp)));
249 
250         if(type == QLatin1String("rect") || type == QLatin1String("oval") || type == QLatin1String("poly"))
251         {
252             QPainterPath boundary = loadLinkBoundary(type, miniexp_cdr(boundaryExp), size);
253 
254             if(!boundary.isEmpty())
255             {
256                 Link* link = loadLinkTarget(boundary, targetExp, index, pageByName);
257 
258                 if(link != 0)
259                 {
260                     links.append(link);
261                 }
262             }
263         }
264     }
265 
266     return links;
267 }
268 
loadText(miniexp_t textExp,QSizeF size,const QRectF & rect)269 QString loadText(miniexp_t textExp, QSizeF size, const QRectF& rect)
270 {
271     if(miniexp_length(textExp) < 6 && !miniexp_symbolp(miniexp_car(textExp)))
272     {
273         return QString();
274     }
275 
276     const int xmin = miniexp_to_int(miniexp_cadr(textExp));
277     const int ymin = miniexp_to_int(miniexp_caddr(textExp));
278     const int xmax = miniexp_to_int(miniexp_cadddr(textExp));
279     const int ymax = miniexp_to_int(miniexp_caddddr(textExp));
280 
281     if(rect.intersects(QRect(xmin, size.height() - ymax, xmax - xmin, ymax - ymin)))
282     {
283         const QString type = QString::fromUtf8(miniexp_to_name(miniexp_car(textExp)));
284 
285         if(type == QLatin1String("word"))
286         {
287             return QString::fromUtf8(miniexp_to_str(miniexp_nth(5, textExp)));
288         }
289         else
290         {
291             QStringList text;
292 
293             textExp = skip(textExp, 5);
294 
295             for(miniexp_t textItem = miniexp_nil; miniexp_consp(textExp); textExp = miniexp_cdr(textExp))
296             {
297                 textItem = miniexp_car(textExp);
298 
299                 text.append(loadText(textItem, size, rect));
300             }
301 
302             return type == QLatin1String("line") ? text.join(" ") : text.join("\n");
303         }
304     }
305 
306     return QString();
307 }
308 
findText(miniexp_t pageTextExp,QSizeF size,const QTransform & transform,const QStringList & words,bool matchCase,bool wholeWords)309 QList< QRectF > findText(miniexp_t pageTextExp, QSizeF size, const QTransform& transform, const QStringList& words, bool matchCase, bool wholeWords)
310 {
311     if(words.isEmpty())
312     {
313         return QList< QRectF >();
314     }
315 
316     const Qt::CaseSensitivity caseSensitivity = matchCase ? Qt::CaseSensitive : Qt::CaseInsensitive;
317 
318     QRectF result;
319     int wordIndex = 0;
320 
321     QList< miniexp_t > texts;
322     QList< QRectF > results;
323 
324     texts.append(pageTextExp);
325 
326     while(!texts.isEmpty())
327     {
328         miniexp_t textExp = texts.takeFirst();
329 
330         if(miniexp_length(textExp) < 6 || !miniexp_symbolp(miniexp_car(textExp)))
331         {
332             continue;
333         }
334 
335         const QString type = QString::fromUtf8(miniexp_to_name(miniexp_car(textExp)));
336 
337         if(type == QLatin1String("word"))
338         {
339             const QString text = QString::fromUtf8(miniexp_to_str(miniexp_nth(5, textExp)));
340 
341             int index = 0;
342 
343             while((index = text.indexOf(words.at(wordIndex), index, caseSensitivity)) != -1)
344             {
345                 const int nextIndex = index + words.at(wordIndex).length();
346 
347                 const bool wordBegins = index == 0 || !text.at(index - 1).isLetterOrNumber();
348                 const bool wordEnds = nextIndex == text.length() || !text.at(nextIndex).isLetterOrNumber();
349 
350                 if(!wholeWords || (wordBegins && wordEnds))
351                 {
352                     const int xmin = miniexp_to_int(miniexp_cadr(textExp));
353                     const int ymin = miniexp_to_int(miniexp_caddr(textExp));
354                     const int xmax = miniexp_to_int(miniexp_cadddr(textExp));
355                     const int ymax = miniexp_to_int(miniexp_caddddr(textExp));
356 
357                     result = result.united(QRectF(xmin, size.height() - ymax, xmax - xmin, ymax - ymin));
358 
359                     // Advance after partial match
360                     if(++wordIndex == words.size())
361                     {
362                         results.append(transform.mapRect(result));
363 
364                         // Reset after full match
365                         result = QRectF();
366                         wordIndex = 0;
367                     }
368                 }
369                 else
370                 {
371                     // Reset after malformed match
372                     result = QRectF();
373                     wordIndex = 0;
374                 }
375 
376                 if((index = nextIndex) >= text.length())
377                 {
378                     break;
379                 }
380             }
381 
382             if(index < 0)
383             {
384                 // Reset after empty match
385                 result = QRectF();
386                 wordIndex = 0;
387             }
388         }
389         else
390         {
391             textExp = skip(textExp, 5);
392 
393             for(miniexp_t textItem = miniexp_nil; miniexp_consp(textExp); textExp = miniexp_cdr(textExp))
394             {
395                 textItem = miniexp_car(textExp);
396 
397                 texts.append(textItem);
398             }
399         }
400     }
401 
402     return results;
403 }
404 
loadOutline(miniexp_t outlineExp,const QHash<QString,int> & pageByName)405 Outline loadOutline(miniexp_t outlineExp, const QHash< QString, int >& pageByName)
406 {
407     Outline outline;
408 
409     for(miniexp_t outlineItem = miniexp_nil; miniexp_consp(outlineExp); outlineExp = miniexp_cdr(outlineExp))
410     {
411         outlineItem = miniexp_car(outlineExp);
412 
413         if(miniexp_length(outlineItem) < 2 || !miniexp_stringp(miniexp_car(outlineItem)) || !miniexp_stringp(miniexp_cadr(outlineItem)))
414         {
415             continue;
416         }
417 
418         const QString title = QString::fromUtf8(miniexp_to_str(miniexp_car(outlineItem)));
419 
420         if(title.isEmpty())
421         {
422             continue;
423         }
424 
425         outline.push_back(Section());
426         Section& section = outline.back();
427         section.title = title;
428 
429         QString destination = QString::fromUtf8(miniexp_to_str(miniexp_cadr(outlineItem)));
430 
431         if(!destination.isEmpty() && destination.at(0) == QLatin1Char('#'))
432         {
433             destination.remove(0, 1);
434 
435             bool ok = false;
436             int page = destination.toInt(&ok);
437 
438             if(!ok)
439             {
440                 const int destinationPage = pageByName.value(destination);
441 
442                 if(destinationPage != 0)
443                 {
444                     ok = true;
445                     page = destinationPage;
446                 }
447             }
448 
449             if(ok)
450             {
451                 section.link.page = page;
452             }
453         }
454 
455         if(miniexp_length(outlineItem) > 2)
456         {
457             section.children = loadOutline(skip(outlineItem, 2), pageByName);
458         }
459     }
460 
461     return outline;
462 }
463 
loadProperties(miniexp_t annoExp)464 Properties loadProperties(miniexp_t annoExp)
465 {
466     Properties properties;
467 
468     for(miniexp_t annoItem = miniexp_nil; miniexp_consp(annoExp); annoExp = miniexp_cdr(annoExp))
469     {
470         annoItem = miniexp_car(annoExp);
471 
472         if(miniexp_length(annoItem) < 2 || qstrcmp(miniexp_to_name(miniexp_car(annoItem)), "metadata") != 0)
473         {
474             continue;
475         }
476 
477         annoItem = miniexp_cdr(annoItem);
478 
479         for(miniexp_t keyValueItem = miniexp_nil; miniexp_consp(annoItem); annoItem = miniexp_cdr(annoItem))
480         {
481             keyValueItem = miniexp_car(annoItem);
482 
483             if(miniexp_length(keyValueItem) != 2)
484             {
485                 continue;
486             }
487 
488             const QString key = QString::fromUtf8(miniexp_to_name(miniexp_car(keyValueItem)));
489             const QString value = QString::fromUtf8(miniexp_to_str(miniexp_cadr(keyValueItem)));
490 
491             if(!key.isEmpty() && !value.isEmpty())
492             {
493                 properties.push_back(qMakePair(key, value));
494             }
495         }
496     }
497 
498     return properties;
499 }
500 
501 } // anonymous
502 
503 namespace qpdfview
504 {
505 
506 namespace Model
507 {
508 
DjVuPage(const DjVuDocument * parent,int index,const ddjvu_pageinfo_t & pageinfo)509 DjVuPage::DjVuPage(const DjVuDocument* parent, int index, const ddjvu_pageinfo_t& pageinfo) :
510     m_parent(parent),
511     m_index(index),
512     m_size(pageinfo.width, pageinfo.height),
513     m_resolution(pageinfo.dpi)
514 {
515 }
516 
~DjVuPage()517 DjVuPage::~DjVuPage()
518 {
519 }
520 
size() const521 QSizeF DjVuPage::size() const
522 {
523     return 72.0 / m_resolution * m_size;
524 }
525 
render(qreal horizontalResolution,qreal verticalResolution,Rotation rotation,QRect boundingRect) const526 QImage DjVuPage::render(qreal horizontalResolution, qreal verticalResolution, Rotation rotation, QRect boundingRect) const
527 {
528     LOCK_PAGE
529 
530     ddjvu_page_t* page = ddjvu_page_create_by_pageno(m_parent->m_document, m_index);
531 
532     if(page == 0)
533     {
534         return QImage();
535     }
536 
537     ddjvu_status_t status;
538 
539     while(true)
540     {
541         status = ddjvu_page_decoding_status(page);
542 
543         if(status < DDJVU_JOB_OK)
544         {
545             clearMessageQueue(m_parent->m_context, true);
546         }
547         else
548         {
549             break;
550         }
551     }
552 
553     if(status >= DDJVU_JOB_FAILED)
554     {
555         ddjvu_page_release(page);
556 
557         return QImage();
558     }
559 
560     switch(rotation)
561     {
562     default:
563     case RotateBy0:
564         ddjvu_page_set_rotation(page, DDJVU_ROTATE_0);
565         break;
566     case RotateBy90:
567         ddjvu_page_set_rotation(page, DDJVU_ROTATE_270);
568         break;
569     case RotateBy180:
570         ddjvu_page_set_rotation(page, DDJVU_ROTATE_180);
571         break;
572     case RotateBy270:
573         ddjvu_page_set_rotation(page, DDJVU_ROTATE_90);
574         break;
575     }
576 
577     ddjvu_rect_t pagerect;
578 
579     pagerect.x = 0;
580     pagerect.y = 0;
581 
582     switch(rotation)
583     {
584     default:
585     case RotateBy0:
586     case RotateBy180:
587         pagerect.w = qRound(horizontalResolution / m_resolution * m_size.width());
588         pagerect.h = qRound(verticalResolution / m_resolution * m_size.height());
589         break;
590     case RotateBy90:
591     case RotateBy270:
592         pagerect.w = qRound(horizontalResolution / m_resolution * m_size.height());
593         pagerect.h = qRound(verticalResolution / m_resolution * m_size.width());
594         break;
595     }
596 
597     ddjvu_rect_t renderrect;
598 
599     if(boundingRect.isNull())
600     {
601         renderrect.x = pagerect.x;
602         renderrect.y = pagerect.y;
603         renderrect.w = pagerect.w;
604         renderrect.h = pagerect.h;
605     }
606     else
607     {
608         renderrect.x = boundingRect.x();
609         renderrect.y = boundingRect.y();
610         renderrect.w = boundingRect.width();
611         renderrect.h = boundingRect.height();
612     }
613 
614     QImage image(renderrect.w, renderrect.h, QImage::Format_RGB32);
615 
616     if(!ddjvu_page_render(page, DDJVU_RENDER_COLOR, &pagerect, &renderrect, m_parent->m_format, image.bytesPerLine(), reinterpret_cast< char* >(image.bits())))
617     {
618         image = QImage();
619     }
620 
621     clearMessageQueue(m_parent->m_context, false);
622 
623     ddjvu_page_release(page);
624 
625     return image;
626 }
627 
label() const628 QString DjVuPage::label() const
629 {
630     return m_parent->m_titleByIndex.value(m_index);
631 }
632 
links() const633 QList< Link* > DjVuPage::links() const
634 {
635     LOCK_PAGE
636 
637     miniexp_t pageAnnoExp = miniexp_nil;
638 
639     {
640         LOCK_PAGE_GLOBAL
641 
642         while(true)
643         {
644             pageAnnoExp = ddjvu_document_get_pageanno(m_parent->m_document, m_index);
645 
646             if(pageAnnoExp == miniexp_dummy)
647             {
648                 clearMessageQueue(m_parent->m_context, true);
649             }
650             else
651             {
652                 break;
653             }
654         }
655     }
656 
657     const QList< Link* > links = loadLinks(pageAnnoExp, m_size, m_index, m_parent->m_pageByName);
658 
659     {
660         LOCK_PAGE_GLOBAL
661 
662         ddjvu_miniexp_release(m_parent->m_document, pageAnnoExp);
663     }
664 
665     return links;
666 }
667 
text(const QRectF & rect) const668 QString DjVuPage::text(const QRectF& rect) const
669 {
670     LOCK_PAGE
671 
672     miniexp_t pageTextExp = miniexp_nil;
673 
674     {
675         LOCK_PAGE_GLOBAL
676 
677         while(true)
678         {
679             pageTextExp = ddjvu_document_get_pagetext(m_parent->m_document, m_index, "word");
680 
681             if(pageTextExp == miniexp_dummy)
682             {
683                 clearMessageQueue(m_parent->m_context, true);
684             }
685             else
686             {
687                 break;
688             }
689         }
690     }
691 
692     const QTransform transform = QTransform::fromScale(m_resolution / 72.0, m_resolution / 72.0);
693 
694     const QString text = loadText(pageTextExp, m_size, transform.mapRect(rect)).simplified();
695 
696     {
697         LOCK_PAGE_GLOBAL
698 
699         ddjvu_miniexp_release(m_parent->m_document, pageTextExp);
700     }
701 
702     return text.simplified();
703 }
704 
search(const QString & text,bool matchCase,bool wholeWords) const705 QList< QRectF > DjVuPage::search(const QString& text, bool matchCase, bool wholeWords) const
706 {
707     LOCK_PAGE
708 
709     miniexp_t pageTextExp = miniexp_nil;
710 
711     {
712         LOCK_PAGE_GLOBAL
713 
714         while(true)
715         {
716             pageTextExp = ddjvu_document_get_pagetext(m_parent->m_document, m_index, "word");
717 
718             if(pageTextExp == miniexp_dummy)
719             {
720                 clearMessageQueue(m_parent->m_context, true);
721             }
722             else
723             {
724                 break;
725             }
726         }
727     }
728 
729     const QTransform transform = QTransform::fromScale(72.0 / m_resolution, 72.0 / m_resolution);
730     const QStringList words = text.split(QRegExp(QLatin1String("\\W+")), QString::SkipEmptyParts);
731 
732     const QList< QRectF > results = findText(pageTextExp, m_size, transform, words, matchCase, wholeWords);
733 
734     {
735         LOCK_PAGE_GLOBAL
736 
737         ddjvu_miniexp_release(m_parent->m_document, pageTextExp);
738     }
739 
740     return results;
741 }
742 
DjVuDocument(QMutex * globalMutex,ddjvu_context_t * context,ddjvu_document_t * document)743 DjVuDocument::DjVuDocument(QMutex* globalMutex, ddjvu_context_t* context, ddjvu_document_t* document) :
744     m_mutex(),
745     m_globalMutex(globalMutex),
746     m_context(context),
747     m_document(document),
748     m_format(0),
749     m_pageByName(),
750     m_titleByIndex()
751 {
752     unsigned int mask[] = {0x00ff0000, 0x0000ff00, 0x000000ff, 0xff000000};
753 
754     m_format = ddjvu_format_create(DDJVU_FORMAT_RGBMASK32, 4, mask);
755     ddjvu_format_set_row_order(m_format, 1);
756     ddjvu_format_set_y_direction(m_format, 1);
757 
758     prepareFileInfo();
759 }
760 
~DjVuDocument()761 DjVuDocument::~DjVuDocument()
762 {
763     ddjvu_document_release(m_document);
764     ddjvu_context_release(m_context);
765     ddjvu_format_release(m_format);
766 }
767 
numberOfPages() const768 int DjVuDocument::numberOfPages() const
769 {
770     LOCK_DOCUMENT
771 
772     return ddjvu_document_get_pagenum(m_document);
773 }
774 
page(int index) const775 Page* DjVuDocument::page(int index) const
776 {
777     LOCK_DOCUMENT
778 
779     ddjvu_status_t status;
780     ddjvu_pageinfo_t pageinfo;
781 
782     while(true)
783     {
784         status = ddjvu_document_get_pageinfo(m_document, index, &pageinfo);
785 
786         if(status < DDJVU_JOB_OK)
787         {
788             clearMessageQueue(m_context, true);
789         }
790         else
791         {
792             break;
793         }
794     }
795 
796     if(status >= DDJVU_JOB_FAILED)
797     {
798         return 0;
799     }
800 
801     return new DjVuPage(this, index, pageinfo);
802 }
803 
saveFilter() const804 QStringList DjVuDocument::saveFilter() const
805 {
806     return QStringList() << QLatin1String("DjVu (*.djvu *.djv)");
807 }
808 
canSave() const809 bool DjVuDocument::canSave() const
810 {
811     return true;
812 }
813 
save(const QString & filePath,bool withChanges) const814 bool DjVuDocument::save(const QString& filePath, bool withChanges) const
815 {
816     Q_UNUSED(withChanges);
817 
818     LOCK_DOCUMENT
819 
820 #ifdef _MSC_VER
821 
822     FILE* file = _wfopen(reinterpret_cast< const wchar_t* >(filePath.utf16()), L"w+");
823 
824 #else
825 
826     FILE* file = fopen(QFile::encodeName(filePath), "w+");
827 
828 #endif // _MSC_VER
829 
830     if(file == 0)
831     {
832         return false;
833     }
834 
835     ddjvu_job_t* job = ddjvu_document_save(m_document, file, 0, 0);
836 
837     while(!ddjvu_job_done(job))
838     {
839         clearMessageQueue(m_context, true);
840     }
841 
842     fclose(file);
843 
844     return !ddjvu_job_error(job);
845 }
846 
outline() const847 Outline DjVuDocument::outline() const
848 {
849     Outline outline;
850 
851     LOCK_DOCUMENT
852 
853     miniexp_t outlineExp = miniexp_nil;
854 
855     {
856         LOCK_DOCUMENT_GLOBAL
857 
858         while(true)
859         {
860             outlineExp = ddjvu_document_get_outline(m_document);
861 
862             if(outlineExp == miniexp_dummy)
863             {
864                 clearMessageQueue(m_context, true);
865             }
866             else
867             {
868                 break;
869             }
870         }
871     }
872 
873     if(miniexp_length(outlineExp) > 1 && qstrcmp(miniexp_to_name(miniexp_car(outlineExp)), "bookmarks") == 0)
874     {
875         outline = loadOutline(skip(outlineExp, 1), m_pageByName);
876     }
877 
878     {
879         LOCK_DOCUMENT_GLOBAL
880 
881         ddjvu_miniexp_release(m_document, outlineExp);
882     }
883 
884     return outline;
885 }
886 
properties() const887 Properties DjVuDocument::properties() const
888 {
889     Properties properties;
890 
891     LOCK_DOCUMENT
892 
893     miniexp_t annoExp = miniexp_nil;
894 
895     {
896         LOCK_DOCUMENT_GLOBAL
897 
898         while(true)
899         {
900             annoExp = ddjvu_document_get_anno(m_document, TRUE);
901 
902             if(annoExp == miniexp_dummy)
903             {
904                 clearMessageQueue(m_context, true);
905             }
906             else
907             {
908                 break;
909             }
910         }
911     }
912 
913     properties = loadProperties(annoExp);
914 
915     {
916         LOCK_DOCUMENT_GLOBAL
917 
918         ddjvu_miniexp_release(m_document, annoExp);
919     }
920 
921     return properties;
922 }
923 
prepareFileInfo()924 void DjVuDocument::prepareFileInfo()
925 {
926     for(int index = 0, count = ddjvu_document_get_filenum(m_document); index < count; ++index)
927     {
928         ddjvu_fileinfo_t fileinfo;
929 
930         if(ddjvu_document_get_fileinfo(m_document, index, &fileinfo) != DDJVU_JOB_OK || fileinfo.type != 'P')
931         {
932             continue;
933         }
934 
935         const QString id = QString::fromUtf8(fileinfo.id);
936         const QString name = QString::fromUtf8(fileinfo.name);
937         const QString title = QString::fromUtf8(fileinfo.title);
938 
939         m_pageByName[id] = m_pageByName[name] = m_pageByName[title] = fileinfo.pageno + 1;
940 
941         if(!title.endsWith(".djvu", Qt::CaseInsensitive) && !title.endsWith(".djv", Qt::CaseInsensitive))
942         {
943             m_titleByIndex[fileinfo.pageno] = title;
944         }
945     }
946 
947     m_pageByName.squeeze();
948     m_titleByIndex.squeeze();
949 }
950 
951 } // Model
952 
DjVuPlugin(QObject * parent)953 DjVuPlugin::DjVuPlugin(QObject* parent) : QObject(parent),
954     m_globalMutex()
955 {
956     setObjectName("DjVuPlugin");
957 }
958 
loadDocument(const QString & filePath) const959 Model::Document* DjVuPlugin::loadDocument(const QString& filePath) const
960 {
961     ddjvu_context_t* context = ddjvu_context_create("qpdfview");
962 
963     if(context == 0)
964     {
965         return 0;
966     }
967 
968 #if DDJVUAPI_VERSION >= 19
969 
970     ddjvu_document_t* document = ddjvu_document_create_by_filename_utf8(context, filePath.toUtf8(), FALSE);
971 
972 #else
973 
974     ddjvu_document_t* document = ddjvu_document_create_by_filename(context, QFile::encodeName(filePath), FALSE);
975 
976 #endif // DDJVUAPI_VERSION
977 
978     if(document == 0)
979     {
980         ddjvu_context_release(context);
981 
982         return 0;
983     }
984 
985     waitForMessageTag(context, DDJVU_DOCINFO);
986 
987     if(ddjvu_document_decoding_error(document))
988     {
989         ddjvu_document_release(document);
990         ddjvu_context_release(context);
991 
992         return 0;
993     }
994 
995     return new Model::DjVuDocument(&m_globalMutex, context, document);
996 }
997 
998 } // qpdfview
999 
1000 #if QT_VERSION < QT_VERSION_CHECK(5,0,0)
1001 
1002 Q_EXPORT_PLUGIN2(qpdfview_djvu, qpdfview::DjVuPlugin)
1003 
1004 #endif // QT_VERSION
1005