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 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 public static PageRange[] computePrintedPages(PageRange[] requestedPages, 398 PageRange[] writtenPages, int pageCount) { 399 // Adjust the print job pages based on what was requested and written. 400 // The cases are ordered in the most expected to the least expected 401 // with a special case first where the app does not know the page count 402 // so we ask for all to be written. 403 if (Arrays.equals(requestedPages, ALL_PAGES_RANGE) 404 && pageCount == PrintDocumentInfo.PAGE_COUNT_UNKNOWN) { 405 return ALL_PAGES_RANGE; 406 } else if (Arrays.equals(writtenPages, requestedPages)) { 407 // We got a document with exactly the pages we wanted. Hence, 408 // the printer has to print all pages in the data. 409 return ALL_PAGES_RANGE; 410 } else if (Arrays.equals(writtenPages, ALL_PAGES_RANGE)) { 411 // We requested specific pages but got all of them. Hence, 412 // the printer has to print only the requested pages. 413 return requestedPages; 414 } else if (PageRangeUtils.contains(writtenPages, requestedPages, pageCount)) { 415 // We requested specific pages and got more but not all pages. 416 // Hence, we have to offset appropriately the printed pages to 417 // be based off the start of the written ones instead of zero. 418 // The written pages are always non-null and not empty. 419 final int offset = -writtenPages[0].getStart(); 420 PageRangeUtils.offset(requestedPages, offset); 421 return requestedPages; 422 } else if (Arrays.equals(requestedPages, ALL_PAGES_RANGE) 423 && isAllPages(writtenPages, pageCount)) { 424 // We requested all pages via the special constant and got all 425 // of them as an explicit enumeration. Hence, the printer has 426 // to print only the requested pages. 427 return ALL_PAGES_RANGE; 428 } 429 430 return null; 431 } 432 } 433