comparison mupdf-source/platform/java/example/ViewerCore.java @ 3:2c135c81b16c

MERGE: upstream PyMuPDF 1.26.4 with MuPDF 1.26.7
author Franz Glasner <fzglas.hg@dom66.de>
date Mon, 15 Sep 2025 11:44:09 +0200
parents b50eed0cc0ef
children
comparison
equal deleted inserted replaced
0:6015a75abc2d 3:2c135c81b16c
1 // Copyright (C) 2004-2025 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 package example;
24
25 import com.artifex.mupdf.fitz.*;
26
27 import java.io.File;
28 import java.util.Vector;
29
30 public class ViewerCore {
31
32 protected Worker worker;
33 protected Callback callback;
34
35 protected String documentPath;
36 protected String acceleratorPath;
37
38 protected Document doc;
39 protected Page page;
40
41 protected Location currentPage;
42 protected Location searchHitPage;
43 protected String searchNeedle;
44
45 protected OutlineItem[] outline;
46
47 protected boolean cancelSearch;
48
49 public ViewerCore(Worker worker, Callback callback, String documentPath) {
50 this.worker = worker;
51 this.callback = callback;
52 this.documentPath = documentPath;
53 }
54
55 public void openDocument(final OnException onException) {
56 worker.add(new Worker.Task() {
57 Document doc = null;
58 String acceleratorPath = null;
59 boolean needsPassword = false;
60 protected String getAcceleratorPath(String documentPath) {
61 try {
62 String accelerator = new File(documentPath).
63 getCanonicalFile().
64 getPath().
65 replace(File.separatorChar, '%').
66 replace('\\', '%').
67 replace('/', '%').
68 replace(':', '%') + ".accel";
69 String tmpdir = System.getProperty("java.io.tmpdir");
70 return new File(tmpdir, accelerator).getCanonicalFile().getPath();
71 } catch (Exception e) {
72 return null;
73 }
74 }
75 protected boolean acceleratorValid(String documentPath, String acceleratorPath) {
76 try {
77 long documentModified = new File(documentPath).lastModified();
78 long acceleratorModified = new File(acceleratorPath).lastModified();
79 return acceleratorModified != 0 && acceleratorModified > documentModified;
80 } catch (Exception e) {
81 return false;
82 }
83 }
84 public void work() {
85 String acceleratorPath = getAcceleratorPath(documentPath);
86 if (!acceleratorValid(documentPath, acceleratorPath))
87 acceleratorPath = null;
88 if (acceleratorPath != null)
89 doc = Document.openDocument(documentPath, acceleratorPath);
90 else
91 doc = Document.openDocument(documentPath);
92 needsPassword = doc.needsPassword();
93 }
94 public void run() {
95 ViewerCore.this.doc = doc;
96 ViewerCore.this.acceleratorPath = acceleratorPath;
97 if (needsPassword) {
98 String password = callback.askPassword();
99 if (password != null)
100 checkPassword(password, onException);
101 }
102 else
103 loadDocument(onException);
104 }
105 public void exception(Throwable t) {
106 ViewerCore.this.acceleratorPath = null;
107 ViewerCore.this.doc = null;
108 if (onException != null)
109 onException.run(t);
110 }
111 });
112 }
113
114 public void reloadDocument(final OnException onException) {
115 acceleratorPath = null;
116 doc = null;
117 page = null;
118 searchHitPage = null;
119 searchNeedle = null;
120 outline = null;
121 cancelSearch = false;
122 openDocument(onException);
123 }
124
125 protected void checkPassword(final String password, final OnException onException) {
126 worker.add(new Worker.Task() {
127 boolean passwordOK = false;
128 public void work() {
129 passwordOK = doc.authenticatePassword(password);
130 }
131 public void run() {
132 if (!passwordOK) {
133 String password = callback.askPassword();
134 if (password != null)
135 checkPassword(password, onException);
136 }
137 else
138 loadDocument(onException);
139 }
140 public void exception(Throwable t) {
141 ViewerCore.this.documentPath = null;
142 ViewerCore.this.acceleratorPath = null;
143 ViewerCore.this.doc = null;
144 if (onException != null)
145 onException.run(t);
146 }
147 });
148 }
149
150 public void refreshMetadata(final OnException onException) {
151 worker.add(new Worker.Task() {
152 String title = "";
153 String author = "";
154 String format = "";
155 String encryption = "";
156 boolean print = false;
157 boolean edit = false;
158 boolean copy = false;
159 boolean annotate = false;
160 boolean form = false;
161 boolean accessibility = false;
162 boolean assemble = false;
163 boolean printHq = false;
164 boolean isPDF = false;
165 boolean reflowable = false;
166 boolean linear = false;
167 int updates = 0;
168 int history = 0;
169 public void work() {
170 title = doc.getMetaData(Document.META_INFO_TITLE);
171 author = doc.getMetaData(Document.META_INFO_AUTHOR);
172 format = doc.getMetaData(Document.META_FORMAT);
173 encryption = doc.getMetaData(Document.META_ENCRYPTION);
174 print = doc.hasPermission(Document.PERMISSION_PRINT);
175 copy = doc.hasPermission(Document.PERMISSION_COPY);
176 edit = doc.hasPermission(Document.PERMISSION_EDIT);
177 annotate = doc.hasPermission(Document.PERMISSION_ANNOTATE);
178 form = doc.hasPermission(Document.PERMISSION_FORM);
179 accessibility = doc.hasPermission(Document.PERMISSION_ACCESSIBILITY);
180 assemble = doc.hasPermission(Document.PERMISSION_ASSEMBLE);
181 printHq = doc.hasPermission(Document.PERMISSION_PRINT_HQ);
182 reflowable = doc.isReflowable();
183 isPDF = doc.isPDF();
184 if (isPDF) {
185 PDFDocument pdf = (PDFDocument) doc;
186 linear = pdf.wasLinearized();
187 updates = pdf.countVersions();
188 history = pdf.validateChangeHistory();
189 }
190 }
191 public void run() {
192 callback.onMetadataChange(title, author, format, encryption);
193 callback.onPermissionsChange(print, copy, edit, annotate, form, accessibility, assemble, printHq);
194 callback.onReflowableChange(reflowable);
195 if (isPDF) {
196 callback.onLinearizedChange(linear);
197 callback.onUpdatesChange(updates, history);
198 }
199 }
200 public void exception(Throwable t) {
201 if (onException != null)
202 onException.run(t);
203 }
204 });
205 }
206
207 protected void loadDocument(final OnException onException) {
208 worker.add(new Worker.Task() {
209 int chapterCount = 0;
210 int pageCount = 0;
211 public void work() {
212 chapterCount = doc.countChapters();
213 pageCount = doc.countPages();
214 if (acceleratorPath != null)
215 doc.saveAccelerator(acceleratorPath);
216 }
217 public void run() {
218 callback.onChapterCountChange(chapterCount);
219 callback.onPageCountChange(pageCount);
220 loadOutline(onException);
221 refreshMetadata(onException);
222 if (currentPage != null)
223 gotoLocation(currentPage, onException);
224 else
225 gotoFirstPage(onException);
226 }
227 public void exception(Throwable t) {
228 if (onException != null)
229 onException.run(t);
230 }
231 });
232 }
233
234 public void relayoutDocument(final int width, final int height, final int em, final OnException onException) {
235 worker.add(new Worker.Task() {
236 int chapterCount = 0;
237 int pageCount = 0;
238 Location newPage;
239 public void work() {
240 long mark = doc.makeBookmark(ViewerCore.this.currentPage);
241 doc.layout(width, height, em);
242 chapterCount = doc.countChapters();
243 pageCount = doc.countPages();
244 newPage = doc.findBookmark(mark);
245 }
246 public void run() {
247 callback.onLayoutChange(width, height, em);
248 callback.onChapterCountChange(chapterCount);
249 callback.onPageCountChange(pageCount);
250 gotoLocation(newPage, onException);
251 loadOutline(onException);
252 }
253 public void exception(Throwable t) {
254 callback.onLayoutChange(450, 600, 12);
255 callback.onChapterCountChange(0);
256 callback.onPageCountChange(0);
257 gotoLocation(null, onException);
258 if (onException != null)
259 onException.run(t);
260 }
261 });
262 }
263
264 protected void loadOutline(final OnException onException) {
265 worker.add(new Worker.Task() {
266 OutlineItem[] outline = null;
267 protected void flattenOutline(Outline[] rawOutline, String indent, Vector<OutlineItem> v) {
268 for (Outline node : rawOutline) {
269 if (node.title != null) {
270 Location loc = doc.resolveLink(node);
271 String title = indent + node.title;
272 v.add(new OutlineItem(title, node.uri, loc));
273 }
274 if (node.down != null)
275 flattenOutline(node.down, indent + " ", v);
276 }
277 }
278 public void work() {
279 Outline[] rawOutline = doc.loadOutline();
280 if (rawOutline != null) {
281 Vector<OutlineItem> v = new Vector<OutlineItem>();
282 flattenOutline(rawOutline, "", v);
283
284 outline = new OutlineItem[v.size()];
285 v.toArray(outline);
286 }
287 }
288 public void run() {
289 ViewerCore.this.outline = outline;
290 callback.onOutlineChange(outline);
291 }
292 public void exception(Throwable t) {
293 callback.onOutlineChange(null);
294 if (onException != null)
295 onException.run(t);
296 }
297 });
298 }
299
300 public void gotoLocation(final Location location, final OnException onException) {
301 worker.add(new Worker.Task() {
302 int itemIndex = -1;
303 Page page = null;
304 Rect bbox = null;
305 int chapterNumber = 0;
306 int pageNumber = 0;
307 public void work() {
308 if (location == null)
309 return;
310
311 chapterNumber = location.chapter;
312 pageNumber = doc.pageNumberFromLocation(location);
313
314 if (outline != null) {
315 for (int i = 0; i < outline.length; ++i) {
316 Location loc = outline[i].location;
317 int outlinePageNumber = doc.pageNumberFromLocation(loc);
318 if (outlinePageNumber <= pageNumber)
319 itemIndex = i;
320 }
321 }
322
323 page = doc.loadPage(location);
324 bbox = page.getBounds();
325 }
326 public void run() {
327 ViewerCore.this.page = page;
328 ViewerCore.this.currentPage = location;
329 if (currentPage == null || !currentPage.equals(searchHitPage))
330 ViewerCore.this.searchHitPage = null;
331 callback.onOutlineItemChange(itemIndex);
332 callback.onPageChange(location, chapterNumber, pageNumber, bbox);
333 }
334 public void exception(Throwable t) {
335 ViewerCore.this.page = null;
336 ViewerCore.this.currentPage = null;
337 ViewerCore.this.searchHitPage = null;
338 callback.onOutlineItemChange(-1);
339 if (onException != null)
340 onException.run(t);
341 }
342 });
343 }
344
345 public void flipPages(final int flips, final OnException onException) {
346 worker.add(new Worker.Task() {
347 Location location = currentPage;
348 public void work() {
349 int page = doc.pageNumberFromLocation(location);
350 int newPage = page + flips;
351 location = doc.locationFromPageNumber(newPage);
352 }
353 public void run() {
354 gotoLocation(location, onException);
355 }
356 public void exception(Throwable t) {
357 if (onException != null)
358 onException.run(t);
359 }
360 });
361 }
362
363 public void gotoFirstPage(final OnException onException) {
364 worker.add(new Worker.Task() {
365 Location location;
366 public void work() {
367 location = doc.locationFromPageNumber(0);
368 }
369 public void run() {
370 gotoLocation(location, onException);
371 }
372 public void exception(Throwable t) {
373 if (onException != null)
374 onException.run(t);
375 }
376 });
377 }
378
379 public void gotoLastPage(final OnException onException) {
380 worker.add(new Worker.Task() {
381 Location location;
382 public void work() {
383 location = doc.lastPage();
384 }
385 public void run() {
386 gotoLocation(location, onException);
387 }
388 public void exception(Throwable t) {
389 if (onException != null)
390 onException.run(t);
391 }
392 });
393 }
394
395 public void gotoPage(final int pageNumber, final OnException onException) {
396 worker.add(new Worker.Task() {
397 Location location;
398 public void work() {
399 location = doc.locationFromPageNumber(pageNumber);
400 }
401 public void run() {
402 gotoLocation(location, onException);
403 }
404 public void exception(Throwable t) {
405 if (onException != null)
406 onException.run(t);
407 }
408 });
409 }
410
411 public void renderPage(final Matrix ctm, final Rect bbox, final boolean icc, final int antialias, final boolean invert, final boolean tint, final int tintBlack, final int tintWhite, final Cookie cookie, final OnException onException) {
412 worker.add(new Worker.Task() {
413 Pixmap pixmap = null;
414 Rect[] links = null;
415 String[] linkURIs = null;
416 Quad[][] hits = null;
417 public void work() {
418 Link[] pageLinks = page.getLinks();
419 if (pageLinks == null)
420 {
421 links = new Rect[0];
422 linkURIs = new String[0];
423 }
424 else
425 {
426 int i = 0;
427 links = new Rect[pageLinks.length];
428 linkURIs = new String[pageLinks.length];
429 for (Link link: pageLinks)
430 {
431 links[i] = link.getBounds().transform(ctm);
432 linkURIs[i] = link.getURI();
433 i++;
434 }
435 }
436
437 if (currentPage.equals(searchHitPage))
438 hits = page.search(searchNeedle);
439 if (hits == null)
440 hits = new Quad[0][];
441 for (Quad[] hit : hits)
442 for (Quad q : hit)
443 q.transform(ctm);
444
445 pixmap = new Pixmap(ColorSpace.DeviceBGR, bbox, true);
446 pixmap.clear(255);
447
448 if (icc)
449 Context.enableICC();
450 else
451 Context.disableICC();
452 Context.setAntiAliasLevel(antialias);
453
454 DrawDevice dev = new DrawDevice(pixmap);
455 page.run(dev, ctm, cookie);
456 dev.close();
457 dev.destroy();
458
459 if (invert) {
460 pixmap.invertLuminance();
461 pixmap.gamma(1 / 1.4f);
462 }
463
464 if (tint)
465 pixmap.tint(tintBlack, tintWhite);
466 }
467 public void run() {
468 callback.onPageContentsChange(pixmap, links, linkURIs, hits);
469 }
470 public void exception(Throwable t) {
471 callback.onPageContentsChange(null, new Rect[0], new String[0], new Quad[0][]);
472 if (onException != null)
473 onException.run(t);
474 }
475 });
476 }
477
478 public void search(final String needle, final int direction, final OnException onException) {
479 cancelSearch = false;
480 if (doc == null)
481 return;
482 worker.add(new Worker.Task() {
483 Location startPage, finalPage;
484 public void work() {
485 if (!currentPage.equals(searchHitPage))
486 startPage = currentPage;
487 else if (direction >= 0)
488 startPage = doc.nextPage(currentPage);
489 else
490 startPage = doc.previousPage(currentPage);
491
492 if (direction >= 0)
493 finalPage = doc.lastPage();
494 else
495 finalPage = doc.locationFromPageNumber(0);
496 }
497 public void run() {
498 callback.onSearchStart(startPage, finalPage, direction, needle);
499 ViewerCore.this.searchNeedle = needle;
500 runSearch(startPage, finalPage, direction, needle, onException);
501 }
502 public void exception(Throwable t) {
503 if (onException != null)
504 onException.run(t);
505 }
506 });
507 }
508
509 public void cancelSearch(final OnException onException) {
510 cancelSearch = true;
511 }
512
513 public void runSearch(final Location startPage, final Location finalPage, final int direction, final String needle, final OnException onException) {
514 worker.add(new Worker.Task() {
515 Location searchPage = startPage;
516 Location hitPage = null;
517 Quad[][] hits = new Quad[0][];
518 boolean done = false;
519 boolean cancelled = false;
520 public void work() {
521 long executionUntil = System.currentTimeMillis() + 100;
522 do {
523 if (cancelSearch) {
524 cancelled = true;
525 continue;
526 }
527
528 Page page = doc.loadPage(searchPage);
529 hits = page.search(needle);
530 if (hits != null && hits.length > 0) {
531 hitPage = searchPage;
532 done = true;
533 continue;
534 }
535 page.destroy();
536
537 if (searchPage.equals(finalPage)) {
538 done = true;
539 }
540 else if (direction >= 0)
541 searchPage = doc.nextPage(searchPage);
542 else
543 searchPage = doc.previousPage(searchPage);
544 } while (!cancelled && !done && System.currentTimeMillis() < executionUntil);
545 }
546 public void run() {
547 if (cancelled) {
548 ViewerCore.this.searchNeedle = null;
549 ViewerCore.this.searchHitPage = null;
550 callback.onSearchCancelled();
551 } else if (done) {
552 ViewerCore.this.searchHitPage = hitPage;
553 callback.onSearchStop(needle, hitPage);
554 } else {
555 callback.onSearchPage(searchPage, needle);
556 worker.add(this);
557 }
558 }
559 public void exception(Throwable t) {
560 callback.onSearchStop(needle, null);
561 if (onException != null)
562 onException.run(t);
563 }
564 });
565 }
566
567 public void save(final String path, final String options, final DocumentWriter.OCRListener ocrListener, final OnException onException) {
568 worker.add(new Worker.Task() {
569 public void work() {
570 PDFDocument pdf = (PDFDocument) doc;
571 pdf.save(path, options);
572 }
573 public void run() {
574 callback.onSaveComplete();
575 }
576 public void exception(Throwable t) {
577 if (onException != null)
578 onException.run(t);
579 }
580 });
581 }
582
583 public void save(final SeekableOutputStream stream, final String options, final DocumentWriter.OCRListener ocrListener, final OnException onException) {
584 worker.add(new Worker.Task() {
585 public void work() {
586 PDFDocument pdf = (PDFDocument) doc;
587 DocumentWriter wri = null;
588 int pageCount = doc.countPages();
589 try {
590 wri = new DocumentWriter(stream, "ocr", "");
591 wri.addOCRListener(ocrListener);
592
593 for (int i = 0; i < pageCount; i++)
594 {
595 Page page = pdf.loadPage(i);
596 Rect bounds = page.getBounds();
597 Device dev = wri.beginPage(bounds);
598 page.run(dev, new Matrix());
599 wri.endPage();
600 }
601
602 wri.close();
603 } finally {
604 if (wri != null) wri.destroy();
605 }
606 }
607 public void run() {
608 callback.onSaveComplete();
609 }
610 public void exception(Throwable t) {
611 if (onException != null)
612 onException.run(t);
613 }
614 });
615 }
616
617 public interface Callback {
618 public String askPassword();
619
620 public void onChapterCountChange(int chapters);
621 public void onLayoutChange(int width, int height, int em);
622 public void onLinearizedChange(boolean linearized);
623 public void onMetadataChange(String title, String author, String format, String encryption);
624 public void onOutlineChange(OutlineItem[] outline);
625 public void onOutlineItemChange(int index);
626 public void onPageChange(Location page, int chapterNumber, int pageNumber, Rect bbox);
627 public void onPageContentsChange(Pixmap pixmap, Rect[] links, String[] linkURIs, Quad[][] searchHits);
628 public void onPageCountChange(int pages);
629 public void onPermissionsChange(boolean print, boolean copy, boolean edit, boolean annotate, boolean form, boolean accessibility, boolean assemble, boolean printHq);
630 public void onReflowableChange(boolean reflowable);
631 public void onSaveComplete();
632 public void onSearchCancelled();
633 public void onSearchPage(Location page, String needle);
634 public void onSearchStart(Location startPage, Location finalPage, int direction, String needle);
635 public void onSearchStop(String needle, Location page);
636 public void onUpdatesChange(int update, int history);
637 }
638
639 public interface OnException {
640 public void run(Throwable t);
641 }
642
643 protected static class OutlineItem {
644 protected String title;
645 protected String uri;
646 protected Location location;
647 public OutlineItem(String title, String uri, Location location) {
648 this.title = title;
649 this.uri = uri;
650 this.location = location;
651 }
652 public String toString() {
653 return title;
654 }
655 }
656 }