///|
priv enum PdfPageSpecToken {
PageSpecNumber(Int)
PageSpecWord(String)
PageSpecDash
PageSpecComma
}
///|
priv struct PdfPageSpecContext {
end_page : Int
document : PdfDocument?
source : String
}
///|
fn PdfPageSpecContext::bad(self : PdfPageSpecContext) -> Unit raise PdfError {
raise BadPageSpecification(self.source)
}
///|
fn pdf_pagespec_is_digit(value : Int) -> Bool {
value >= 48 && value <= 57
}
///|
fn pdf_pagespec_is_separator(value : Int) -> Bool {
value == 45 || value == 44 || pdf_is_whitespace_byte(value)
}
///|
fn pdf_pagespec_parse_nonnegative_int(text : String) -> Int? {
if text.length() == 0 {
None
} else {
let mut valid = true
for char in text {
let value = char.to_int()
if !pdf_pagespec_is_digit(value) {
valid = false
}
}
if !valid {
None
} else {
let parsed : Result[Int, Error] = try? @strconv.from_str(text)
match parsed {
Ok(value) if value >= 0 => Some(value)
_ => None
}
}
}
}
///|
fn pdf_pagespec_token_of_view(
view : BytesView,
context : PdfPageSpecContext,
) -> PdfPageSpecToken raise PdfError {
let text = @ascii.decode_lossy(view)
match pdf_pagespec_parse_nonnegative_int(text) {
Some(value) => PageSpecNumber(value)
None =>
if text.length() == 0 {
context.bad()
PageSpecWord("")
} else {
PageSpecWord(text)
}
}
}
///|
fn pdf_pagespec_tokenize(
text : String,
context : PdfPageSpecContext,
) -> Array[PdfPageSpecToken] raise PdfError {
let bytes = @utf8.encode(text)
let tokens : Array[PdfPageSpecToken] = []
let mut index = 0
while index < bytes.length() {
let value = bytes[index].to_int()
if pdf_is_whitespace_byte(value) {
index += 1
} else if value == 45 {
tokens.push(PageSpecDash)
index += 1
} else if value == 44 {
tokens.push(PageSpecComma)
index += 1
} else {
let start = index
while index < bytes.length() &&
!pdf_pagespec_is_separator(bytes[index].to_int()) {
index += 1
}
tokens.push(pdf_pagespec_token_of_view(bytes[start:index], context))
}
}
tokens
}
///|
fn pdf_pagespec_range(start : Int, finish : Int) -> Array[Int] {
let output : Array[Int] = []
if start <= finish {
let mut page = start
while page <= finish {
output.push(page)
page += 1
}
} else {
let mut page = start
while page >= finish {
output.push(page)
page -= 1
}
}
output
}
///|
fn pdf_pagespec_all_pages(end_page : Int) -> Array[Int] {
pdf_pagespec_range(1, end_page)
}
///|
fn pdf_pagespec_invert_range(
end_page : Int,
range : ArrayView[Int],
) -> Array[Int] {
let output : Array[Int] = []
for page in 1..<=end_page {
let mut found = false
for selected in range {
if selected == page {
found = true
}
}
if !found {
output.push(page)
}
}
output
}
///|
pub fn pdf_invert_page_range(
end_page : Int,
range : ArrayView[Int],
) -> Array[Int] {
pdf_pagespec_invert_range(end_page, range)
}
///|
fn pdf_pagespec_duplicate_range(
count : Int,
range : ArrayView[Int],
) -> Array[Int] {
let output : Array[Int] = []
for page in range {
for _ in 0.. Array[Int] {
let output : Array[Int] = []
for page in range {
if page % 2 != 0 {
output.push(page)
}
}
output
}
///|
fn pdf_pagespec_filter_even(range : ArrayView[Int]) -> Array[Int] {
let output : Array[Int] = []
for page in range {
if page % 2 == 0 {
output.push(page)
}
}
output
}
///|
fn pdf_pagespec_token_number(
context : PdfPageSpecContext,
token : PdfPageSpecToken,
) -> Int? raise PdfError {
match token {
PageSpecNumber(value) => Some(value)
PageSpecWord("end") => Some(context.end_page)
PageSpecWord(text) if text.has_prefix("~") => {
let rest = text.unsafe_substring(start=1, end=text.length())
match pdf_pagespec_parse_nonnegative_int(rest) {
Some(value) => Some(context.end_page + 1 - value)
None => {
context.bad()
None
}
}
}
_ => None
}
}
///|
fn pdf_pagespec_has_comma(tokens : ArrayView[PdfPageSpecToken]) -> Bool {
for token in tokens {
if token is PageSpecComma {
break true
}
} nobreak {
false
}
}
///|
fn pdf_pagespec_split_commas(
tokens : ArrayView[PdfPageSpecToken],
context : PdfPageSpecContext,
) -> Array[Array[PdfPageSpecToken]] raise PdfError {
let parts : Array[Array[PdfPageSpecToken]] = []
let mut current : Array[PdfPageSpecToken] = []
for token in tokens {
if token is PageSpecComma {
if current.length() == 0 {
context.bad()
}
parts.push(current)
current = []
} else {
current.push(token)
}
}
if current.length() == 0 {
context.bad()
}
parts.push(current)
parts
}
///|
fn PdfPageSpecContext::select_dimensions(
self : PdfPageSpecContext,
candidates : ArrayView[Int],
portrait : Bool,
) -> Array[Int] raise PdfError {
match self.document {
Some(document) => {
let pages = document.pages_of_pagetree()
let output : Array[Int] = []
for page_number in candidates {
if page_number >= 1 && page_number <= pages.length() {
let page = pages[page_number - 1]
let rectangle = document.parse_rectangle(page.mediabox)
let width = rectangle.max_x - rectangle.min_x
let height = rectangle.max_y - rectangle.min_y
if (portrait && width < height) || (!portrait && width > height) {
output.push(page_number)
}
}
}
output
}
None => {
self.bad()
[]
}
}
}
///|
fn PdfPageSpecContext::select_annotated(
self : PdfPageSpecContext,
candidates : ArrayView[Int],
) -> Array[Int] raise PdfError {
match self.document {
Some(document) => {
let pages = document.pages_of_pagetree()
let output : Array[Int] = []
for page_number in candidates {
if page_number >= 1 && page_number <= pages.length() {
let page = pages[page_number - 1]
match document.lookup_direct(pdf_page_annots_key(), page.rest) {
Some(PdfArray(annotations)) if annotations.length() > 0 =>
output.push(page_number)
_ => ()
}
}
}
output
}
None => {
self.bad()
[]
}
}
}
///|
fn PdfPageSpecContext::parse_tokens(
self : PdfPageSpecContext,
tokens : ArrayView[PdfPageSpecToken],
) -> Array[Int] raise PdfError {
if tokens.length() == 0 {
self.bad()
}
if pdf_pagespec_has_comma(tokens) {
let output : Array[Int] = []
for part in pdf_pagespec_split_commas(tokens, self) {
for page in self.parse_tokens(part) {
output.push(page)
}
}
output
} else if tokens.length() == 1 {
match tokens[0] {
PageSpecWord("all") => pdf_pagespec_all_pages(self.end_page)
PageSpecWord("reverse") => pdf_pagespec_range(self.end_page, 1)
PageSpecWord("empty") => []
PageSpecWord("odd") =>
pdf_pagespec_filter_odd(pdf_pagespec_all_pages(self.end_page))
PageSpecWord("even") =>
pdf_pagespec_filter_even(pdf_pagespec_all_pages(self.end_page))
PageSpecWord("portrait") =>
self.select_dimensions(pdf_pagespec_all_pages(self.end_page), true)
PageSpecWord("landscape") =>
self.select_dimensions(pdf_pagespec_all_pages(self.end_page), false)
PageSpecWord("annotated") =>
self.select_annotated(pdf_pagespec_all_pages(self.end_page))
token =>
match pdf_pagespec_token_number(self, token) {
Some(page) => [page]
None => {
self.bad()
[]
}
}
}
} else if tokens.length() == 3 && tokens[1] is PageSpecDash {
match
(
pdf_pagespec_token_number(self, tokens[0]),
pdf_pagespec_token_number(self, tokens[2]),
) {
(Some(start), Some(finish)) => pdf_pagespec_range(start, finish)
_ => {
self.bad()
[]
}
}
} else {
self.bad()
[]
}
}
///|
fn pdf_pagespec_digit_dup_prefix(text : String) -> (Int, String)? {
let bytes = @utf8.encode(text)
let mut index = 0
while index < bytes.length() && pdf_pagespec_is_digit(bytes[index].to_int()) {
index += 1
}
if index > 0 &&
index + 3 <= bytes.length() &&
bytes[index].to_int() == 68 &&
bytes[index + 1].to_int() == 85 &&
bytes[index + 2].to_int() == 80 {
let count_text = @ascii.decode_lossy(bytes[0:index])
match pdf_pagespec_parse_nonnegative_int(count_text) {
Some(count) =>
Some((count, text.unsafe_substring(start=index + 3, end=text.length())))
None => None
}
} else {
None
}
}
///|
fn PdfPageSpecContext::resolve_page_labels(
self : PdfPageSpecContext,
spec : BytesView,
) -> Bytes raise PdfError {
match self.document {
None => {
for byte in spec {
if byte.to_int() == 91 || byte.to_int() == 93 {
self.bad()
}
}
spec.to_owned()
}
Some(document) => {
let labels = document.read_page_labels()
let output : Array[Byte] = []
let mut index = 0
while index < spec.length() {
let value = spec[index].to_int()
if value == 91 {
index += 1
let label : Array[Byte] = []
let mut closed = false
let mut done = false
while index < spec.length() && !done {
let current = spec[index].to_int()
if current == 93 {
closed = true
done = true
index += 1
} else if current == 92 &&
index + 1 < spec.length() &&
(spec[index + 1].to_int() == 91 || spec[index + 1].to_int() == 93) {
label.push(spec[index + 1])
index += 2
} else if current == 91 {
self.bad()
} else {
label.push(spec[index])
index += 1
}
}
if !closed {
self.bad()
}
let label_bytes = Bytes::from_array(label)
let mut found = false
for page_number in 1..<=self.end_page {
let page_label = match
pdf_page_label_text_of_page_number(page_number, labels) {
Some(value) => value
None => Bytes::new(0)
}
if !found && page_label[:] == label_bytes[:] {
pdf_writer_push_ascii(output, page_number.to_string())
found = true
}
}
if !found {
self.bad()
}
} else if value == 92 &&
index + 1 < spec.length() &&
(spec[index + 1].to_int() == 91 || spec[index + 1].to_int() == 93) {
output.push(spec[index + 1])
index += 2
} else if value == 93 {
self.bad()
} else {
output.push(spec[index])
index += 1
}
}
Bytes::from_array(output)
}
}
}
///|
fn PdfPageSpecContext::filter_valid_pages(
self : PdfPageSpecContext,
pages : ArrayView[Int],
) -> Array[Int] {
let output : Array[Int] = []
for page in pages {
if page > 0 && page <= self.end_page {
output.push(page)
}
}
output
}
///|
fn PdfPageSpecContext::parse_resolved_text(
self : PdfPageSpecContext,
text : String,
) -> Array[Int] raise PdfError {
match pdf_pagespec_digit_dup_prefix(text) {
Some((count, rest)) =>
return pdf_pagespec_duplicate_range(count, self.parse_inner(rest))
None => ()
}
if text.has_prefix("NOT") {
return pdf_pagespec_invert_range(
self.end_page,
self.parse_inner(text.unsafe_substring(start=3, end=text.length())),
)
}
if text == "odd" {
return pdf_pagespec_filter_odd(pdf_pagespec_all_pages(self.end_page))
}
if text == "even" {
return pdf_pagespec_filter_even(pdf_pagespec_all_pages(self.end_page))
}
if text == "portrait" {
return self.select_dimensions(pdf_pagespec_all_pages(self.end_page), true)
}
if text == "landscape" {
return self.select_dimensions(pdf_pagespec_all_pages(self.end_page), false)
}
if text == "annotated" {
return self.select_annotated(pdf_pagespec_all_pages(self.end_page))
}
if text.has_suffix("odd") {
return pdf_pagespec_filter_odd(
self.parse_inner(text.unsafe_substring(start=0, end=text.length() - 3)),
)
}
if text.has_suffix("even") {
return pdf_pagespec_filter_even(
self.parse_inner(text.unsafe_substring(start=0, end=text.length() - 4)),
)
}
if text.has_suffix("portrait") {
return self.select_dimensions(
self.parse_inner(text.unsafe_substring(start=0, end=text.length() - 8)),
true,
)
}
if text.has_suffix("landscape") {
return self.select_dimensions(
self.parse_inner(text.unsafe_substring(start=0, end=text.length() - 9)),
false,
)
}
self.parse_tokens(pdf_pagespec_tokenize(text, self))
}
///|
fn PdfPageSpecContext::parse_inner(
self : PdfPageSpecContext,
spec : String,
) -> Array[Int] raise PdfError {
let effective = if spec == "" { "all" } else { spec }
let resolved = self.resolve_page_labels(@utf8.encode(effective))
let resolved_text = @ascii.decode_lossy(resolved)
self.filter_valid_pages(self.parse_resolved_text(resolved_text))
}
///|
pub fn pdf_parse_pagespec_for_count(
end_page : Int,
spec : String,
) -> Array[Int] raise PdfError {
if end_page < 1 {
raise BadPageSpecification(spec)
}
{ end_page, document: None, source: spec }.parse_inner(spec)
}
///|
pub fn pdf_parse_pagespec_without_pdf(
spec : String,
) -> Array[Int] raise PdfError {
pdf_parse_pagespec_for_count(500000, spec)
}
///|
pub fn PdfDocument::parse_pagespec(
self : PdfDocument,
spec : String,
) -> Array[Int] raise PdfError {
let end_page = self.endpage()
if end_page < 1 {
raise BadPageSpecification(spec)
}
{ end_page, document: Some(self), source: spec }.parse_inner(spec)
}
///|
pub fn pdf_parse_pagespec(
document : PdfDocument,
spec : String,
) -> Array[Int] raise PdfError {
document.parse_pagespec(spec)
}
///|
fn pdf_pagespec_same_values(
left : ArrayView[Int],
right : ArrayView[Int],
) -> Bool {
if left.length() != right.length() {
false
} else {
for index in 0.. String {
let mut output = ""
for index in 0.. 0 {
output = output + ","
}
if start == finish {
output = output + start.to_string()
} else {
output = output + start.to_string() + "-" + finish.to_string()
}
}
output
}
///|
pub fn pdf_string_of_pagespec_for_count(
end_page : Int,
pages : ArrayView[Int],
) -> String {
if pages.length() == 0 {
""
} else {
let sorted = pages.to_owned()
sorted.sort()
if end_page > 3 &&
pdf_pagespec_same_values(
sorted,
pdf_pagespec_filter_even(pdf_pagespec_all_pages(end_page)),
) {
"even"
} else if end_page > 2 &&
pdf_pagespec_same_values(
sorted,
pdf_pagespec_filter_odd(pdf_pagespec_all_pages(end_page)),
) {
"odd"
} else if pdf_pagespec_same_values(sorted, pdf_pagespec_all_pages(end_page)) {
"all"
} else {
let ranges : Array[(Int, Int)] = []
let mut start = sorted[0]
let mut previous = sorted[0]
for index in 1.. String raise PdfError {
pdf_string_of_pagespec_for_count(self.endpage(), pages)
}
///|
pub fn pdf_string_of_pagespec(
document : PdfDocument,
pages : ArrayView[Int],
) -> String raise PdfError {
document.string_of_pagespec(pages)
}