///|
/// PDF object serialization functions.
///|
fn trim_trailing_zeros(value : String) -> String {
let dot = value.find(".")
match dot {
None => value
Some(_) => {
let chars = value[:]
let mut end = chars.length() - 1
while end >= 0 && chars[end] == '0' {
end = end - 1
}
if end >= 0 && chars[end] == '.' {
end = end - 1
}
if end < 0 {
"0"
} else {
slice_range(value, 0, end + 1)
}
}
}
}
///|
fn slice_from(value : String, start : Int) -> String {
try value[start:] catch {
_ => ""
} noraise {
view => view.to_string()
}
}
///|
fn slice_range(value : String, start : Int, end : Int) -> String {
try value[start:end] catch {
_ => ""
} noraise {
view => view.to_string()
}
}
///|
fn parse_signed_int(value : String) -> Int {
let chars = value[:]
if chars.length() == 0 {
return 0
}
let mut sign = 1
let mut i = 0
let first = chars[0]
if first is '-' {
sign = -1
i = 1
} else if first is '+' {
i = 1
}
let mut result = 0
while i < chars.length() {
let c = chars[i]
if c < '0' || c > '9' {
break
}
result = result * 10 + (c.to_int() - '0'.to_int())
i = i + 1
}
sign * result
}
///|
fn expand_scientific(value : String) -> String {
let (sign, body) = match value.get_char(0) {
Some('-') => ("-", slice_from(value, 1))
_ => ("", value)
}
let exp_index = match body.find("e") {
Some(i) => Some(i)
None => body.find("E")
}
match exp_index {
None => sign + trim_trailing_zeros(body)
Some(idx) => {
let mantissa = slice_range(body, 0, idx)
let exp_str = slice_from(body, idx + 1)
let exp = parse_signed_int(exp_str)
let digits = Array::new()
let mut int_len = 0
let mut saw_dot = false
for ch in mantissa {
if ch == '.' {
saw_dot = true
} else {
if !saw_dot {
int_len = int_len + 1
}
digits.push(ch)
}
}
if digits.length() == 0 {
sign + "0"
} else {
let new_pos = int_len + exp
let sb = StringBuilder::new()
sb.write_string(sign)
if new_pos <= 0 {
sb.write_string("0.")
for _ in 0..<-new_pos {
sb.write_char('0')
}
for ch in digits {
sb.write_char(ch)
}
} else if new_pos >= digits.length() {
for ch in digits {
sb.write_char(ch)
}
for _ in 0..<(new_pos - digits.length()) {
sb.write_char('0')
}
} else {
for i in 0.. String {
if value == 0.0 {
return "0"
}
let raw = value.to_string()
if raw.contains("e") || raw.contains("E") {
expand_scientific(raw)
} else {
trim_trailing_zeros(raw)
}
}
///|
fn is_delimiter_byte(b : Int) -> Bool {
match b {
b if b == '('.to_int() => true
b if b == ')'.to_int() => true
b if b == '<'.to_int() => true
b if b == '>'.to_int() => true
b if b == '['.to_int() => true
b if b == ']'.to_int() => true
b if b == '{'.to_int() => true
b if b == '}'.to_int() => true
b if b == '/'.to_int() => true
b if b == '%'.to_int() => true
_ => false
}
}
///|
fn hex_digit(value : Int) -> Char {
match value {
0 => '0'
1 => '1'
2 => '2'
3 => '3'
4 => '4'
5 => '5'
6 => '6'
7 => '7'
8 => '8'
9 => '9'
10 => 'A'
11 => 'B'
12 => 'C'
13 => 'D'
14 => 'E'
15 => 'F'
_ => '0'
}
}
///|
fn needs_processing_name(bytes : @pdfio.MutableBytes) -> Bool {
if bytes.length() < 2 {
return false
}
for i in 1.. 126 || is_delimiter_byte(b) || b == '#'.to_int() {
return true
}
}
false
}
///|
fn make_pdf_name(name : String) -> String {
let normalized = match name.get_char(0) {
Some('/') => name
_ => {
@pdfe.log("warning: bad name |\{name}|\n")
"/" + name
}
}
let bytes = @pdfio.bytes_of_string(normalized)
if !needs_processing_name(bytes) {
return normalized
}
let sb = StringBuilder::new()
sb.write_char('/')
for i in 1.. 126 || is_delimiter_byte(b) || b == '#'.to_int() {
sb.write_char('#')
sb.write_char(hex_digit(b / 16))
sb.write_char(hex_digit(b % 16))
} else {
sb.write_char(b.unsafe_to_char())
}
}
sb.to_string()
}
///|
fn make_pdf_string(value : String) -> String {
let sb = StringBuilder::new()
sb.write_char('(')
for ch in value {
match ch {
'(' | ')' | '\\' => {
sb.write_char('\\')
sb.write_char(ch)
}
'\n' => sb.write_string("\\n")
'\r' => sb.write_string("\\r")
'\t' => sb.write_string("\\t")
'\b' => sb.write_string("\\b")
'\u000C' => sb.write_string("\\f")
_ => sb.write_char(ch)
}
}
sb.write_char(')')
sb.to_string()
}
///|
fn write_pdf_object(
obj : PdfObject,
include_stream_data : Bool,
warn_on_toget : Bool,
sb : StringBuilder,
) -> Unit {
match obj {
PdfObject::Null => sb.write_string("null")
PdfObject::Boolean(b) => sb.write_string(if b { "true" } else { "false" })
PdfObject::Integer(i) => sb.write_string(i.to_string())
PdfObject::Real(r) => sb.write_string(format_real(r))
PdfObject::String(s) => sb.write_string(make_pdf_string(s))
PdfObject::Name(n) => sb.write_string(make_pdf_name(n.to_string_bytes()))
PdfObject::Array(values) => {
sb.write_char('[')
for i in 0.. 0 {
sb.write_char(' ')
}
write_pdf_object(values[i], include_stream_data, warn_on_toget, sb)
}
sb.write_char(']')
}
PdfObject::Dictionary(entries) => {
sb.write_string("<<")
for i in 0.. 0 {
sb.write_char(' ')
}
sb.write_string(make_pdf_name(key))
sb.write_char(' ')
write_pdf_object(value, include_stream_data, warn_on_toget, sb)
}
sb.write_string(">>")
}
PdfObject::Stream(r) => {
let (dict, _) = r.val
write_pdf_object(dict, include_stream_data, warn_on_toget, sb)
sb.write_string("\nstream\n")
if include_stream_data {
let _ = obj.getstream() catch { _ => () }
match r.val {
(_, Stream::Got(bytes)) =>
sb.write_string(@pdfio.string_of_bytes(bytes))
_ =>
if warn_on_toget {
@pdfe.log("WARNING: toget in string_of_pdfobj_including_data\n")
}
}
}
sb.write_string("\nendstream")
}
PdfObject::Indirect(n) => sb.write_string("\{n} 0 R")
}
}
///|
/// Convert a PDF object to its string representation.
pub fn PdfObject::to_string(self : PdfObject) -> String {
let sb = StringBuilder::new()
write_pdf_object(self, false, false, sb)
sb.to_string()
}
///|
/// Convert a PDF object to string including stream data.
pub fn PdfObject::to_string_including_data(self : PdfObject) -> String {
let sb = StringBuilder::new()
write_pdf_object(self, true, true, sb)
sb.to_string()
}
///|
test "serialize helpers handle trimming and slicing" {
assert_eq(trim_trailing_zeros("123"), "123")
assert_eq(trim_trailing_zeros("1.23000"), "1.23")
assert_eq(trim_trailing_zeros("0.000"), "0")
assert_eq(slice_from("x", 2), "")
assert_eq(slice_range("x", 0, 5), "")
assert_eq(parse_signed_int(""), 0)
assert_eq(parse_signed_int("+12x"), 12)
assert_eq(parse_signed_int("-3"), -3)
}
///|
test "expand_scientific formats exponent strings" {
assert_eq(expand_scientific("-1.2300"), "-1.23")
assert_eq(expand_scientific("e1"), "0")
assert_eq(expand_scientific("1e-3"), "0.001")
assert_eq(expand_scientific("1e3"), "1000")
assert_eq(expand_scientific("12.34e1"), "123.4")
}