1 /*
2  * Copyright (C) 2014 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package com.android.printspooler.util;
18 
19 import android.print.PageRange;
20 import android.print.PrintDocumentInfo;
21 import android.util.Pair;
22 
23 import java.util.ArrayList;
24 import java.util.Arrays;
25 import java.util.Comparator;
26 
27 /**
28  * This class contains utility functions for working with page ranges.
29  */
30 public final class PageRangeUtils {
31 
32     private static final PageRange[] ALL_PAGES_RANGE = new PageRange[] {PageRange.ALL_PAGES};
33 
34     private static final Comparator<PageRange> sComparator = new Comparator<PageRange>() {
35         @Override
36         public int compare(PageRange lhs, PageRange rhs) {
37             return lhs.getStart() - rhs.getStart();
38         }
39     };
40 
PageRangeUtils()41     private PageRangeUtils() {
42         /* do nothing - hide constructor */
43     }
44 
45     /**
46      * Gets whether page ranges contains a given page.
47      *
48      * @param pageRanges The page ranges.
49      * @param pageIndex The page for which to check.
50      * @return Whether the page is within the ranges.
51      */
contains(PageRange[] pageRanges, int pageIndex)52     public static boolean contains(PageRange[] pageRanges, int pageIndex) {
53         final int rangeCount = pageRanges.length;
54         for (int i = 0; i < rangeCount; i++) {
55             PageRange pageRange = pageRanges[i];
56             if (pageRange.contains(pageIndex)) {
57                 return true;
58             }
59         }
60         return false;
61     }
62 
63     /**
64      * Checks whether one page range array contains another one.
65      *
66      * @param ourRanges The container page ranges.
67      * @param otherRanges The contained page ranges.
68      * @param pageCount The total number of pages.
69      * @return Whether the container page ranges contains the contained ones.
70      */
contains(PageRange[] ourRanges, PageRange[] otherRanges, int pageCount)71     public static boolean contains(PageRange[] ourRanges, PageRange[] otherRanges, int pageCount) {
72         if (ourRanges == null || otherRanges == null) {
73             return false;
74         }
75 
76         if (Arrays.equals(ourRanges, ALL_PAGES_RANGE)) {
77             return true;
78         }
79 
80         if (Arrays.equals(otherRanges, ALL_PAGES_RANGE)) {
81             otherRanges[0] = new PageRange(0, pageCount - 1);
82         }
83 
84         ourRanges = normalize(ourRanges);
85         otherRanges = normalize(otherRanges);
86 
87         // Note that the code below relies on the ranges being normalized
88         // which is they contain monotonically increasing non-intersecting
89         // sub-ranges whose start is less that or equal to the end.
90         int otherRangeIdx = 0;
91         final int ourRangeCount = ourRanges.length;
92         final int otherRangeCount = otherRanges.length;
93         for (int ourRangeIdx = 0; ourRangeIdx < ourRangeCount; ourRangeIdx++) {
94             PageRange ourRange = ourRanges[ourRangeIdx];
95             for (; otherRangeIdx < otherRangeCount; otherRangeIdx++) {
96                 PageRange otherRange = otherRanges[otherRangeIdx];
97                 if (otherRange.getStart() > ourRange.getEnd()) {
98                     break;
99                 }
100                 if (otherRange.getStart() < ourRange.getStart()
101                         || otherRange.getEnd() > ourRange.getEnd()) {
102                     return false;
103                 }
104             }
105         }
106         return (otherRangeIdx >= otherRangeCount);
107     }
108 
109     /**
110      * Normalizes a page range, which is the resulting page ranges are
111      * non-overlapping with the start lesser than or equal to the end
112      * and ordered in an ascending order.
113      *
114      * @param pageRanges The page ranges to normalize.
115      * @return The normalized page ranges.
116      */
normalize(PageRange[] pageRanges)117     public static PageRange[] normalize(PageRange[] pageRanges) {
118         if (pageRanges == null) {
119             return null;
120         }
121 
122         final int oldRangeCount = pageRanges.length;
123         if (oldRangeCount <= 1) {
124             return pageRanges;
125         }
126 
127         Arrays.sort(pageRanges, sComparator);
128 
129         int newRangeCount = 1;
130         for (int i = 0; i < oldRangeCount - 1; i++) {
131             PageRange currentRange = pageRanges[i];
132             PageRange nextRange = pageRanges[i + 1];
133             if (currentRange.getEnd() + 1 >= nextRange.getStart()) {
134                 pageRanges[i] = null;
135                 pageRanges[i + 1] = new PageRange(currentRange.getStart(),
136                         Math.max(currentRange.getEnd(), nextRange.getEnd()));
137             } else {
138                 newRangeCount++;
139             }
140         }
141 
142         if (newRangeCount == oldRangeCount) {
143             return pageRanges;
144         }
145 
146         int normalRangeIndex = 0;
147         PageRange[] normalRanges = new PageRange[newRangeCount];
148         for (int i = 0; i < oldRangeCount; i++) {
149             PageRange normalRange = pageRanges[i];
150             if (normalRange != null) {
151                 normalRanges[normalRangeIndex] = normalRange;
152                 normalRangeIndex++;
153             }
154         }
155 
156         return normalRanges;
157     }
158 
159     /**
160      * Return the next position after {@code pos} that is not a space character.
161      *
162      * @param s   The string to parse
163      * @param pos The starting position
164      *
165      * @return The position of the first space character
166      */
readWhiteSpace(CharSequence s, int pos)167     private static int readWhiteSpace(CharSequence s, int pos) {
168         while (pos < s.length() && s.charAt(pos) == ' ') {
169             pos++;
170         }
171 
172         return pos;
173     }
174 
175     /**
176      * Read a number from a string at a certain position.
177      *
178      * @param s   The string to parse
179      * @param pos The starting position
180      *
181      * @return The position after the number + the number read or null if the number was not found
182      */
readNumber(CharSequence s, int pos)183     private static Pair<Integer, Integer> readNumber(CharSequence s, int pos) {
184         Integer result = 0;
185         while (pos < s.length() && s.charAt(pos) >= '0' && s.charAt(pos) <= '9') {
186             // Number cannot start with 0
187             if (result == 0 && s.charAt(pos) == '0') {
188                 break;
189             }
190             result = result * 10 + (s.charAt(pos) - '0');
191             // Abort on overflow
192             if (result < 0) {
193                 break;
194             }
195             pos++;
196         }
197 
198         // 0 is not a valid page number
199         if (result == 0) {
200             return new Pair<>(pos, null);
201         } else {
202             return new Pair<>(pos, result);
203         }
204     }
205 
206     /**
207      * Read a single character from a string at a certain position.
208      *
209      * @param s            The string to parse
210      * @param pos          The starting position
211      * @param expectedChar The character to read
212      *
213      * @return The position after the character + the character read or null if the character was
214      *         not found
215      */
readChar(CharSequence s, int pos, char expectedChar)216     private static Pair<Integer, Character> readChar(CharSequence s, int pos, char expectedChar) {
217         if (pos < s.length() && s.charAt(pos) == expectedChar) {
218             return new Pair<>(pos + 1, expectedChar);
219         } else {
220             return new Pair<>(pos, null);
221         }
222     }
223 
224     /**
225      * Read a page range character from a string at a certain position.
226      *
227      * @param s             The string to parse
228      * @param pos           The starting position
229      * @param maxPageNumber The highest page number to accept.
230      *
231      * @return The position after the page range + the page range read or null if the page range was
232      *         not found
233      */
readRange(CharSequence s, int pos, int maxPageNumber)234     private static Pair<Integer, PageRange> readRange(CharSequence s, int pos, int maxPageNumber) {
235         Pair<Integer, Integer> retInt;
236         Pair<Integer, Character> retChar;
237 
238         Character comma;
239         if (pos == 0) {
240             // When we reading the first range, we do not want to have a comma
241             comma = ',';
242         } else {
243             retChar = readChar(s, pos, ',');
244             pos = retChar.first;
245             comma = retChar.second;
246         }
247 
248         pos = readWhiteSpace(s, pos);
249 
250         retInt = readNumber(s, pos);
251         pos = retInt.first;
252         Integer start = retInt.second;
253 
254         pos = readWhiteSpace(s, pos);
255 
256         retChar = readChar(s, pos, '-');
257         pos = retChar.first;
258         Character separator = retChar.second;
259 
260         pos = readWhiteSpace(s, pos);
261 
262         retInt = readNumber(s, pos);
263         pos = retInt.first;
264         Integer end = retInt.second;
265 
266         pos = readWhiteSpace(s, pos);
267 
268         if (comma != null &&
269                 // range, maybe unbounded
270                 ((separator != null && (start != null || end != null)) ||
271                         // single page
272                         (separator == null && start != null && end == null))) {
273             if (start == null) {
274                 start = 1;
275             }
276 
277             if (end == null) {
278                 if (separator == null) {
279                     end = start;
280                 } else {
281                     end = maxPageNumber;
282                 }
283             }
284 
285             if (start <= end && start >= 1 && end <= maxPageNumber) {
286                 return new Pair<>(pos, new PageRange(start - 1, end - 1));
287             }
288         }
289 
290         return new Pair<>(pos, null);
291     }
292 
293     /**
294      * Parse a string into an array of page ranges.
295      *
296      * @param s             The string to parse
297      * @param maxPageNumber The highest page number to accept.
298      *
299      * @return The parsed ranges or null if the string could not be parsed.
300      */
parsePageRanges(CharSequence s, int maxPageNumber)301     public static PageRange[] parsePageRanges(CharSequence s, int maxPageNumber) {
302         ArrayList<PageRange> ranges = new ArrayList<>();
303 
304         int pos = 0;
305         while (pos < s.length()) {
306             Pair<Integer, PageRange> retRange = readRange(s, pos, maxPageNumber);
307 
308             if (retRange.second == null) {
309                 ranges.clear();
310                 break;
311             }
312 
313             ranges.add(retRange.second);
314             pos = retRange.first;
315         }
316 
317         return PageRangeUtils.normalize(ranges.toArray(new PageRange[ranges.size()]));
318     }
319 
320     /**
321      * Offsets a the start and end of page ranges with the given value.
322      *
323      * @param pageRanges The page ranges to offset.
324      * @param offset The offset value.
325      */
offset(PageRange[] pageRanges, int offset)326     public static void offset(PageRange[] pageRanges, int offset) {
327         if (offset == 0) {
328             return;
329         }
330         final int pageRangeCount = pageRanges.length;
331         for (int i = 0; i < pageRangeCount; i++) {
332             final int start = pageRanges[i].getStart() + offset;
333             final int end = pageRanges[i].getEnd() + offset;
334             pageRanges[i] = new PageRange(start, end);
335         }
336     }
337 
338     /**
339      * Gets the number of pages in a normalized range array.
340      *
341      * @param pageRanges Normalized page ranges.
342      * @param layoutPageCount Page count after reported after layout pass.
343      * @return The page count in the ranges.
344      */
getNormalizedPageCount(PageRange[] pageRanges, int layoutPageCount)345     public static int getNormalizedPageCount(PageRange[] pageRanges, int layoutPageCount) {
346         int pageCount = 0;
347         if (pageRanges != null) {
348             final int pageRangeCount = pageRanges.length;
349             for (int i = 0; i < pageRangeCount; i++) {
350                 PageRange pageRange = pageRanges[i];
351                 if (PageRange.ALL_PAGES.equals(pageRange)) {
352                     return layoutPageCount;
353                 }
354                 pageCount += pageRange.getSize();
355             }
356         }
357         return pageCount;
358     }
359 
asAbsoluteRange(PageRange pageRange, int pageCount)360     public static PageRange asAbsoluteRange(PageRange pageRange, int pageCount) {
361         if (PageRange.ALL_PAGES.equals(pageRange)) {
362             return new PageRange(0, pageCount - 1);
363         }
364         return pageRange;
365     }
366 
isAllPages(PageRange[] pageRanges)367     public static boolean isAllPages(PageRange[] pageRanges) {
368         final int pageRangeCount = pageRanges.length;
369         for (int i = 0; i < pageRangeCount; i++) {
370             PageRange pageRange = pageRanges[i];
371             if (isAllPages(pageRange)) {
372                 return true;
373             }
374         }
375         return false;
376     }
377 
isAllPages(PageRange pageRange)378     public static boolean isAllPages(PageRange pageRange) {
379         return PageRange.ALL_PAGES.equals(pageRange);
380     }
381 
isAllPages(PageRange[] pageRanges, int pageCount)382     public static boolean isAllPages(PageRange[] pageRanges, int pageCount) {
383         final int pageRangeCount = pageRanges.length;
384         for (int i = 0; i < pageRangeCount; i++) {
385             PageRange pageRange = pageRanges[i];
386             if (isAllPages(pageRange, pageCount)) {
387                 return true;
388             }
389         }
390         return false;
391     }
392 
isAllPages(PageRange pageRanges, int pageCount)393     public static boolean isAllPages(PageRange pageRanges, int pageCount) {
394         return pageRanges.getStart() == 0 && pageRanges.getEnd() == pageCount - 1;
395     }
396 
397     /**
398      * Compute the pages of the file that correspond to the requested pages in the doc.
399      *
400      * @param pagesInDocRequested The requested pages, doc-indexed
401      * @param pagesWrittenToFile The pages in the file
402      * @param pageCount The number of pages in the doc
403      *
404      * @return The pages, file-indexed
405      */
computeWhichPagesInFileToPrint(PageRange[] pagesInDocRequested, PageRange[] pagesWrittenToFile, int pageCount)406     public static PageRange[] computeWhichPagesInFileToPrint(PageRange[] pagesInDocRequested,
407             PageRange[] pagesWrittenToFile, int pageCount) {
408         // Adjust the print job pages based on what was requested and written.
409         // The cases are ordered in the most expected to the least expected
410         // with a special case first where the app does not know the page count
411         // so we ask for all to be written.
412         if (Arrays.equals(pagesInDocRequested, ALL_PAGES_RANGE)
413                 && pageCount == PrintDocumentInfo.PAGE_COUNT_UNKNOWN) {
414             return ALL_PAGES_RANGE;
415         } else if (Arrays.equals(pagesWrittenToFile, pagesInDocRequested)) {
416             // We got a document with exactly the pages we wanted. Hence,
417             // the printer has to print all pages in the data.
418             return ALL_PAGES_RANGE;
419         } else if (Arrays.equals(pagesWrittenToFile, ALL_PAGES_RANGE)) {
420             // We requested specific pages but got all of them. Hence,
421             // the printer has to print only the requested pages.
422             return pagesInDocRequested;
423         } else if (PageRangeUtils.contains(pagesWrittenToFile, pagesInDocRequested, pageCount)) {
424             // We requested specific pages and got more but not all pages.
425             // Hence, we have to offset appropriately the printed pages to
426             // be based off the start of the written ones instead of zero.
427             // The written pages are always non-null and not empty.
428             final int offset = -pagesWrittenToFile[0].getStart();
429             PageRangeUtils.offset(pagesInDocRequested.clone(), offset);
430             return pagesInDocRequested;
431         } else if (Arrays.equals(pagesInDocRequested, ALL_PAGES_RANGE)
432                 && isAllPages(pagesWrittenToFile, pageCount)) {
433             // We requested all pages via the special constant and got all
434             // of them as an explicit enumeration. Hence, the printer has
435             // to print only the requested pages.
436             return ALL_PAGES_RANGE;
437         }
438 
439         return null;
440     }
441 }
442