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
17package com.android.printspooler.util;
18
19import android.print.PageRange;
20import android.print.PrintDocumentInfo;
21import android.util.Pair;
22
23import java.util.ArrayList;
24import java.util.Arrays;
25import java.util.Comparator;
26
27/**
28 * This class contains utility functions for working with page ranges.
29 */
30public 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
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     */
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     */
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     */
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     */
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     */
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     */
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     */
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     */
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     */
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     */
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
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
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
378    public static boolean isAllPages(PageRange pageRange) {
379        return PageRange.ALL_PAGES.equals(pageRange);
380    }
381
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
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     */
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