📜 ⬆️ ⬇️

Monotouch OpenSource library for displaying PDF documents

Well, finally I decided to make one of my projects open and accessible to the public, i.e. turn it into open source, but I wanted to tell about this event on Habré.
The following discussion focuses on the Monotouch platform and the display of PDF documents in iOS . In the topic, I will not give a bunch of library sources, who needs it, he will find them on GitHub, but I will try to focus on the complex issues that arise when building a library.


Surely many iOS developers using Monotouch have been faced with the task of displaying PDF documents in their programs. Some time ago, and I was faced with such a task when developing my program. Studying the proposed standard features for displaying PDF documents in iOS (see the comparative table below), I realized that writing my library is indispensable. Immediately I want to say, when I created my library, iOS 5 has not yet been released, and I did not have a new PageControl component at hand.

In the end, I decided to have patience (a lot of coffee), and started to write my bicycle masterpiece. Before creating the library, I defined a list of its capabilities:

Of course, I didn’t have time or couldn’t realize all my ideas; perhaps my hands will reach later, someday. The definition of the links and content of the document, as well as the search in the document, are not yet implemented.

Turning pages


The first, and perhaps the main question was how to implement the visual “flipping” of pages? As mentioned above, the PageControl component has not yet existed, so I had to reinvent my solution. Thinking, I decided to use the standard ScrollView with a large content area and several View in it. ScrollView allows you to add several View to it, when moving, they are visually replaced (here is the effect of turning). How many views were needed? The answer is simple - three (one for the previous one, the second for the current one, the third for the next page). The content area in this case is 3 * View.Width, i.e. triple width page view.
')


I implemented horizontal and vertical page turning. Actually flipping occurs when dragging the current View to the left or up (to turn forward) and right or down (to turn back).

The algorithm is simple. All this business is implemented in DocumentViewController, the successor of UIScrollViewController. I use a list (not an array) for storing the View. When you move the current View (when the next page is not the first and not the last), a new View is created and added to the list, the desired PDF page is loaded into the new View, and the unused View (the first in the list) is deleted from the list. In addition, SrollView.Position is shifted to the display position of the central, i.e. second on the View list.

public virtual void OpenDocumentPage(int pageNumber) { if (PDFDocument.DocumentHasLoaded && (pageNumber != PDFDocument.CurrentPageNumber)) { if ((pageNumber < 1) || (pageNumber > PDFDocument.PageCount)) { return; } // Set current page PDFDocument.CurrentPageNumber = pageNumber; // Calc min, max page int minValue; int maxValue; if (PDFDocument.PageCount <= MaxPageViewsCount) { minValue = 1; maxValue = PDFDocument.PageCount; } else { minValue = PDFDocument.CurrentPageNumber - 1; maxValue = PDFDocument.CurrentPageNumber + 1; if (minValue < 1) { minValue++; maxValue++; } else if (maxValue > PDFDocument.PageCount) { minValue--; maxValue--; } } // Create/update page views for displayed pages var unusedPageViews = new List<PageView>(mPageViews); RectangleF viewRect = GetScrollViewSubViewFrame(); for (int i = minValue, j = 0; i <= maxValue; i++,j++) { PageView pageView = mPageViews.FirstOrDefault(v => v.PageNumber == i); if (pageView == null) { pageView = new PageView(viewRect, i); mScrollView.AddSubview(pageView); mPageViews.Add(pageView); } else { pageView.Frame = viewRect; pageView.PageNumber = i; pageView.ZoomReset(); unusedPageViews.Remove(pageView); } viewRect = CalcFrameForNextPage(viewRect); if (i == PDFDocument.CurrentPageNumber) { mCurrentPageView = pageView; } } // Clear unused page views foreach (var view in unusedPageViews) { view.RemoveFromSuperview(); mPageViews.Remove(view); } // Update scroll view content offset UpdateScrollViewContentOffset(); } } 


High-quality display and page scaling




View to display the page, in fact, consists of as many as three View:

The first View is needed to scale the page, the second one is actually for the quality display of the page, the third one is needed for the temporary page image.

For high-quality display of the page when scaling, the heir of the CATiledLayer class was used. This class allows you to display detailed content when you zoom in View. But, you have to pay for everything - using CATiledLayer led to a very long primary page rendering (a few seconds), which caused unpleasant flickering and step-by-step replacement of the old image with a new one. To avoid this negative effect, I added another View - ThumbView, into which I made the initial output of a PDF page with a low resolution, and only then the formation and output of the main page image:

The actual display of the PDF page itself is very simple:

 private void Draw(CGContext context) { if (!PDFDocument.DocumentHasLoaded) { return; } // Draw page context.SetFillColor(1.0f, 1.0f, 1.0f, 1.0f); using (CGPDFPage pdfPage = PDFDocument.GetPage(mPageNumber)) { context.TranslateCTM(0, Bounds.Height); context.ScaleCTM(1.0f, -1.0f); context.ConcatCTM(pdfPage.GetDrawingTransform(CGPDFBox.Crop, Bounds, 0, true)); context.SetRenderingIntent(CGColorRenderingIntent.Default); context.InterpolationQuality = CGInterpolationQuality.Default; context.DrawPDFPage(pdfPage); } } 


Bookmarks and notes



When designing classes (managers) for working with bookmarks and notes of a document, I wanted to provide an opportunity for developers to implement their own data storage mechanisms: in xml, database or other. In my application using the library, I save data on bookmarks and notes in the Sqlite database. In a demo application, data is stored in memory while the application is running.

Actually, both managers are similar to each other. They have methods for loading, saving and deleting data. Most of the methods are virtual. The methods operate on the date - the DocumentBookmark and DocumentNote objects, respectively.

Because, in Monotouch, there is no such wonderful thing as Generic types, the mechanism for activating managers, I don’t quite like it. I created the ObjectActivator class, in which there are methods for creating manager instances when the application starts. Accordingly, in the case of the inheritance of manager classes, new instances must be registered in ObjectActivator:

 public class MyObjectsActivator : ObjectsActivator { /// <summary> /// Returns DocumentNoteManager instance /// </summary> /// <returns></returns> protected override DocumentNoteManager CreateDocumentNoteManager() { return new MyDocumentNoteManager(); } /// <summary> /// Returns DocumentBookmarkManager instance /// </summary> /// <returns></returns> protected override DocumentBookmarkManager CreateDocumentBookmarkManager() { return new MyDocumentBookmarkManager(); } } 


Page thumbnails




To display thumbnails of pages, the same approach is used as to display the pages themselves, with the exception that in the case of thumbnails more than one page and a slightly different algorithm for creating new Views are displayed at a time. In addition, the width of the ScrollView content area for thumbnails is the sum of the width of all page thumbnails plus indents.

Honestly, I had to tinker with the display of thumbnails. At first I tried to implement an algorithm in which the number of thumbnails would be equal to the number of displayed, plus one on the left and one on the right (i.e., an algorithm that is exactly the same as the page display). At the same time, for faster rendering, more unnecessary miniatures (that is, hidden ones), located from the opposite side of the movement, would not be removed, but moved towards the direction of movement. Those. would make some whirlpool. From this idea had to be abandoned, because when scrolling through thumbnails quickly, the Scrolled event was triggered with gaps of 100 or more pixels, which resulted in “gaps” between thumbnails, i.e. to their wrong location.

As a result, I stopped at the “buffer” algorithm, in which the thumbnails displayed at the moment always remain in place, and in the case of movement, they are replaced only when the buffer is filled. The buffer size is set in the options, so for greater performance it should be increased (of course due to the additional memory consumption).

Total


In general, that's all, if you have any questions / comments / suggestions please write in the comments, with pleasure (not always of course) I will answer.


GitHub library sources: github.com/AlexanderMac/mTouch-PDFReader

Source: https://habr.com/ru/post/142941/


All Articles