use mlua::{Lua, MultiValue, Table, Value};
use retex_engine::Engine;
use retex_engine::backend as bk;
use retex_engine::pdfext::{LuaPdfTexts, ObjKind, PdfObject, literal_mode};
use crate::{create_function_plain_errors, with_engine};
#[derive(Clone, Copy)]
enum IntRule {
Any,
Clamp(i32, i32),
Accept(i32, i32),
}
const INT_VARS: &[(&str, u32, IntRule)] = &[
("compresslevel", bk::COMPRESS_LEVEL, IntRule::Clamp(0, 9)),
(
"decimaldigits",
bk::DECIMAL_DIGITS,
IntRule::Clamp(0, i32::MAX),
),
("imageresolution", 139, IntRule::Any),
("majorversion", bk::MAJOR_VERSION, IntRule::Accept(1, 2)),
("minorversion", bk::MINOR_VERSION, IntRule::Accept(0, 9)),
("inclusionerrorlevel", 145, IntRule::Any),
("ignoreunknownimages", 146, IntRule::Any),
(
"objcompresslevel",
bk::OBJ_COMPRESS_LEVEL,
IntRule::Clamp(0, 9),
),
("gentounicode", 154, IntRule::Any),
("suppressoptionalinfo", 156, IntRule::Any),
("omitcidset", 157, IntRule::Any),
("recompress", 158, IntRule::Clamp(0, 9)),
("omitcharset", 159, IntRule::Any),
("omitinfo", 160, IntRule::Any),
("omitmediabox", 161, IntRule::Any),
("omitprocset", 163, IntRule::Any),
("ptexprefix", 164, IntRule::Any),
];
const PK_RESOLUTION: u32 = 140;
const PK_FIXED_DPI: u32 = 155;
const DIMEN_VARS: &[(&str, u32)] = &[
("threadmargin", 30),
("destmargin", 31),
("linkmargin", 32),
("xformmargin", 33),
];
type TextField = fn(&mut LuaPdfTexts) -> &mut Option<Vec<u8>>;
const TEXT_VARS: &[(&str, TextField)] = &[
("catalog", |t| &mut t.catalog),
("info", |t| &mut t.info),
("names", |t| &mut t.names),
("trailer", |t| &mut t.trailer),
("trailerid", |t| &mut t.trailer_id),
("pageattributes", |t| &mut t.page_attributes),
("pageresources", |t| &mut t.page_resources),
("pagesattributes", |t| &mut t.pages_attributes),
("xformattributes", |t| &mut t.xform_attributes),
("xformresources", |t| &mut t.xform_resources),
];
const MODES: &[(&str, u8)] = &[
("origin", literal_mode::ORIGIN),
("page", literal_mode::PAGE),
("direct", literal_mode::DIRECT),
("raw", literal_mode::RAW),
("text", 4),
];
fn err<T>(msg: impl Into<String>) -> mlua::Result<T> {
Err(mlua::Error::runtime(msg.into()))
}
fn int_value(v: &Value) -> Option<i32> {
match *v {
Value::Integer(i) => Some(i as i32),
Value::Number(f) => Some(exact_integer(f).unwrap_or(0) as i32),
_ => None,
}
}
fn exact_integer(f: f64) -> Option<i64> {
(f.fract() == 0.0 && f >= -(2f64.powi(63)) && f < 2f64.powi(63)).then_some(f as i64)
}
fn dimen_value(v: &Value) -> Option<i32> {
let x = match *v {
Value::Integer(i) => i as f64,
Value::Number(f) => f,
_ => return None,
};
let r = (x + 0.5).floor();
Some(
if r.is_nan() || r < f64::from(i32::MIN) || r > f64::from(i32::MAX) {
i32::MIN
} else {
r as i32
},
)
}
fn integer_number(v: &Value) -> Option<i64> {
match *v {
Value::Integer(i) => Some(i),
Value::Number(f) => exact_integer(f),
_ => None,
}
}
fn check_integer(lua: &Lua, v: Option<&Value>, argn: usize, fname: &str) -> mlua::Result<i64> {
let bad = |msg: String| err(format!("bad argument #{argn} to '{fname}' ({msg})"));
let Some(v) = v else {
return bad("number expected, got no value".into());
};
if let Some(i) = lua.coerce_integer(v.clone())? {
return Ok(i);
}
if lua.coerce_number(v.clone())?.is_some() {
return bad("number has no integer representation".into());
}
bad(format!("number expected, got {}", v.type_name()))
}
fn coerce_bytes(lua: &Lua, v: &Value) -> mlua::Result<Option<Vec<u8>>> {
Ok(match v {
Value::String(s) => Some(s.as_bytes().to_vec()),
Value::Integer(_) | Value::Number(_) => {
lua.coerce_string(v.clone())?.map(|s| s.as_bytes().to_vec())
}
_ => None,
})
}
fn font_arg(lua: &Lua, args: &MultiValue) -> mlua::Result<Option<u32>> {
let Some(f) = args.front().and_then(int_value) else {
return Ok(None);
};
with_engine(lua, |e| {
(f >= 0 && (f == 0 || e.font_exists(f))).then_some(f as u32)
})
}
pub fn install(lua: &Lua) -> mlua::Result<()> {
let t = lua.create_table()?;
install_variables(lua, &t)?;
install_feedback(lua, &t)?;
install_objects(lua, &t)?;
install_output(lua, &t)?;
lua.globals().set("pdf", t)?;
Ok(())
}
fn install_variables(lua: &Lua, t: &Table) -> mlua::Result<()> {
for &(name, index, rule) in INT_VARS {
t.set(
format!("get{name}"),
create_function_plain_errors(lua, move |lua, ()| {
with_engine(lua, |e| i64::from(e.eqtb.int(index)))
})?,
)?;
t.set(
format!("set{name}"),
create_function_plain_errors(lua, move |lua, args: MultiValue| {
let Some(v) = args.front().and_then(int_value) else {
return Ok(());
};
let v = match rule {
IntRule::Any => v,
IntRule::Clamp(lo, hi) => v.clamp(lo, hi),
IntRule::Accept(lo, hi) if (lo..=hi).contains(&v) => v,
IntRule::Accept(..) => return Ok(()),
};
with_engine(lua, |e| e.eqtb.int_par[index as usize].val = v)
})?,
)?;
}
t.set(
"getpkresolution",
create_function_plain_errors(lua, |lua, ()| {
with_engine(lua, |e| {
(
i64::from(e.eqtb.int(PK_RESOLUTION)),
i64::from(e.eqtb.int(PK_FIXED_DPI)),
)
})
})?,
)?;
t.set(
"setpkresolution",
create_function_plain_errors(lua, |lua, args: MultiValue| {
let Some(v) = args.front().and_then(int_value) else {
return Ok(());
};
let fixed = matches!(args.get(1), Some(Value::Integer(_) | Value::Number(_)));
with_engine(lua, |e| {
e.eqtb.int_par[PK_RESOLUTION as usize].val = v.clamp(72, 8000);
if fixed {
e.eqtb.int_par[PK_FIXED_DPI as usize].val = v;
}
})
})?,
)?;
for &(name, index) in DIMEN_VARS {
t.set(
format!("get{name}"),
create_function_plain_errors(lua, move |lua, ()| {
with_engine(lua, |e| i64::from(e.eqtb.dimen(index)))
})?,
)?;
t.set(
format!("set{name}"),
create_function_plain_errors(lua, move |lua, args: MultiValue| {
let Some(v) = args.front().and_then(dimen_value) else {
return Ok(());
};
with_engine(lua, |e| e.eqtb.dimen_par[index as usize].val = v)
})?,
)?;
}
t.set(
"getorigin",
create_function_plain_errors(lua, |lua, ()| {
with_engine(lua, |e| {
(
i64::from(e.eqtb.dimen(bk::H_ORIGIN)),
i64::from(e.eqtb.dimen(bk::V_ORIGIN)),
)
})
})?,
)?;
t.set(
"setorigin",
create_function_plain_errors(lua, |lua, args: MultiValue| {
let (h, v) = match args.front().and_then(dimen_value) {
Some(h) => (h, args.get(1).and_then(dimen_value).unwrap_or(h)),
None => (0, 0),
};
with_engine(lua, |e| {
e.eqtb.dimen_par[bk::H_ORIGIN as usize].val = h;
e.eqtb.dimen_par[bk::V_ORIGIN as usize].val = v;
})
})?,
)?;
for &(name, field) in TEXT_VARS {
t.set(
format!("get{name}"),
create_function_plain_errors(lua, move |lua, ()| {
let v = with_engine(lua, |e| field(&mut e.pdf.lua).clone())?;
v.map(|b| lua.create_string(&b)).transpose()
})?,
)?;
t.set(
format!("set{name}"),
create_function_plain_errors(lua, move |lua, args: MultiValue| {
let Some(Value::String(s)) = args.back() else {
return Ok(());
};
let s = s.as_bytes().to_vec();
with_engine(lua, |e| *field(&mut e.pdf.lua) = Some(s))
})?,
)?;
}
t.set(
"getcreationdate",
create_function_plain_errors(lua, |lua, ()| with_engine(lua, |e| e.start_time.pdf_date()))?,
)?;
Ok(())
}
fn install_feedback(lua: &Lua, t: &Table) -> mlua::Result<()> {
type Getter = fn(&Engine) -> i32;
let ints: &[(&str, Getter)] = &[
("getlastobj", |e| e.pdf.last_obj),
("getlastlink", |e| e.pdf.last_link),
("getlastannot", |e| e.pdf.last_annot),
("getretval", |e| e.pdf.retval),
("getmaxobjnum", |e| e.pdf.max_obj()),
("gethpos", |e| e.pdf.pos.0),
("getvpos", |e| e.pdf.pos.1),
];
for &(name, get) in ints {
t.set(
name,
create_function_plain_errors(lua, move |lua, ()| {
with_engine(lua, |e| i64::from(get(e)))
})?,
)?;
}
t.set("maxobjnum", t.get::<mlua::Function>("getmaxobjnum")?)?;
t.set(
"getpos",
create_function_plain_errors(lua, |lua, ()| {
with_engine(lua, |e| (i64::from(e.pdf.pos.0), i64::from(e.pdf.pos.1)))
})?,
)?;
t.set(
"getnofobjects",
create_function_plain_errors(lua, |lua, ()| {
with_engine(lua, |e| {
let written = e.pdf.objects.values().filter(|o| o.written).count() as i64;
(written, i64::from(e.pdf.max_obj()) - written)
})
})?,
)?;
t.set(
"getmatrix",
create_function_plain_errors(lua, |_, ()| Ok((1, 0, 0, 1, 0, 0)))?,
)?;
t.set(
"hasmatrix",
create_function_plain_errors(lua, |_, ()| Ok(false))?,
)?;
let objtype = create_function_plain_errors(lua, |lua, args: MultiValue| {
let Some(n) = args.front().and_then(integer_number) else {
return Ok(None);
};
let n = i32::try_from(n).unwrap_or(0);
with_engine(lua, |e| e.pdf_obj_kind(n).map(ObjKind::name))
})?;
t.set("getobjtype", objtype.clone())?;
t.set("objtype", objtype)?;
let pageref = create_function_plain_errors(lua, |lua, args: MultiValue| {
let Some(n) = args.front().and_then(integer_number) else {
return Ok(None);
};
if n < 1 || n > i64::from(i32::MAX) {
return Ok(None);
}
with_engine(lua, |e| Some(i64::from(e.pdf_page_ref(n as i32))))
})?;
t.set("getpageref", pageref.clone())?;
t.set("pageref", pageref)?;
let xformname = create_function_plain_errors(lua, |_, _: MultiValue| Ok(Value::Nil))?;
t.set("getxformname", xformname.clone())?;
t.set("xformname", xformname)?;
let fontname = create_function_plain_errors(lua, |lua, args: MultiValue| {
let Some(f) = font_arg(lua, &args)? else {
return Ok(None);
};
with_engine(lua, |e| Some(i64::from(e.pdf_font(f).0)))
})?;
t.set("getfontname", fontname.clone())?;
t.set("fontname", fontname)?;
let fontobjnum = create_function_plain_errors(lua, |lua, args: MultiValue| {
let Some(f) = font_arg(lua, &args)? else {
return Ok(None);
};
with_engine(lua, |e| Some(i64::from(e.pdf_font(f).1)))
})?;
t.set("getfontobjnum", fontobjnum.clone())?;
t.set("fontobjnum", fontobjnum)?;
let fontsize = create_function_plain_errors(lua, |lua, args: MultiValue| {
let Some(f) = font_arg(lua, &args)? else {
return Ok(None);
};
with_engine(lua, |e| Some(i64::from(e.font(f).size)))
})?;
t.set("getfontsize", fontsize.clone())?;
t.set("fontsize", fontsize)?;
Ok(())
}
fn reserved_obj(e: &Engine, n: i64, fname: &str) -> Result<i32, String> {
let n = i32::try_from(n).map_err(|_| format!("{fname} invalid object number"))?;
match (e.pdf_obj_kind(n), e.pdf.objects.get(&n)) {
(Some(ObjKind::Obj), Some(o)) if !o.defined => Ok(n),
(Some(ObjKind::Obj), _) => Err(format!("{fname} object in use")),
_ => Err(format!("{fname} invalid object number")),
}
}
fn check_pdf_mode(lua: &Lua) -> mlua::Result<()> {
let mode = with_engine(lua, |e| {
e.eqtb
.int(retex_engine::primitives_gen::assign_int::OUTPUTMODE)
})?;
if mode <= 0 {
return err("immediateobj() not allowed in DVI mode (outputmode = 0)");
}
Ok(())
}
fn claim_reserved(e: &mut Engine, n: i64, fname: &str) -> Result<i32, String> {
let n = reserved_obj(e, n, fname)?;
if let Some(o) = e.pdf.objects.get_mut(&n) {
o.defined = true;
}
Ok(n)
}
fn alloc_obj(e: &mut Engine) -> i32 {
let n = e.pdf_alloc_obj(ObjKind::Obj);
e.pdf.last_obj = n;
n
}
fn positional_obj(lua: &Lua, args: &[Value], immediate: bool) -> mlua::Result<i64> {
let fname = if immediate {
"pdf.immediateobj()"
} else {
"pdf.obj()"
};
if immediate {
check_pdf_mode(lua)?;
}
let (given, rest) = match args.first() {
Some(v @ (Value::Integer(_) | Value::Number(_))) => {
(Some(integer_number(v).unwrap_or(0)), &args[1..])
}
_ => (None, args),
};
let num = match given {
Some(n) => match with_engine(lua, |e| claim_reserved(e, n, fname))? {
Ok(n) => n,
Err(m) => return err(m),
},
None => with_engine(lua, alloc_obj)?,
};
if rest.is_empty() {
return err(format!("{fname} needs at least one argument"));
}
if rest.len() > 3 {
return err(format!("{fname} allows max. 3 arguments"));
}
let ordinal = ["1st", "2nd", "3rd"];
let mut strings = Vec::new();
for (i, v) in rest.iter().enumerate() {
match coerce_bytes(lua, v)? {
Some(s) => strings.push(s),
None => return err(format!("{fname} {} argument must be string", ordinal[i])),
}
}
let mut obj = PdfObject {
num,
defined: true,
..Default::default()
};
match (strings[0].as_slice(), strings.len()) {
(_, 1) => obj.data = strings.remove(0),
(b"file", 2) => {
obj.file = true;
obj.data = strings.remove(1);
}
(b"stream" | b"streamfile", _) => {
obj.stream = true;
obj.file = strings[0] == b"streamfile";
obj.attr = strings.get(2).cloned().unwrap_or_default();
obj.data = strings.swap_remove(1);
}
_ => {
if immediate {
with_engine(lua, |e| e.pdf_define_object(obj, true))?;
}
return err(format!("{fname} invalid argument"));
}
}
with_engine(lua, |e| e.pdf_define_object(obj, immediate))?;
Ok(i64::from(num))
}
fn table_obj(lua: &Lua, t: &Table) -> mlua::Result<i64> {
let stream = match t.get::<Value>("type")? {
Value::Nil => return err("pdf.obj(): object \"type\" missing"),
Value::String(s) => match s.as_bytes().as_ref() {
b"raw" => false,
b"stream" => true,
other => {
return err(format!(
"pdf.obj(): \"{}\" is not a valid object type",
String::from_utf8_lossy(other)
));
}
},
_ => return err("pdf.obj(): object \"type\" must be string"),
};
let immediate = matches!(t.get::<Value>("immediate")?, Value::Boolean(true));
if immediate {
check_pdf_mode(lua)?;
}
let num = match integer_number(&t.get::<Value>("objnum")?) {
Some(n) => match with_engine(lua, |e| claim_reserved(e, n, "pdf.obj()"))? {
Ok(n) => n,
Err(m) => return err(m),
},
None => with_engine(lua, alloc_obj)?,
};
let string = coerce_bytes(lua, &t.get::<Value>("string")?)?;
let file = coerce_bytes(lua, &t.get::<Value>("file")?)?;
let attr = coerce_bytes(lua, &t.get::<Value>("attr")?)?;
if string.is_some() && file.is_some() {
return err("pdf.obj(): \"string\" and \"file\" must not be given together");
}
if attr.is_some() && !stream {
return err("pdf.obj(): \"attr\" key not allowed for non-stream object");
}
if let Some(l) = integer_number(&t.get::<Value>("compresslevel")?)
&& l > 9
{
return err("pdf.obj(): \"compresslevel\" must be <= 9");
}
let (data, is_file) = match (string, file) {
(Some(s), None) => (s, false),
(None, Some(f)) => (f, true),
_ => return err("pdf.obj(): no \"string\" or \"file\" given"),
};
let obj = PdfObject {
num,
data,
stream,
attr: attr.unwrap_or_default(),
file: is_file,
uncompressed: matches!(t.get::<Value>("objcompression")?, Value::Boolean(false)),
defined: true,
written: false,
};
with_engine(lua, |e| e.pdf_define_object(obj, immediate))?;
Ok(i64::from(num))
}
fn install_objects(lua: &Lua, t: &Table) -> mlua::Result<()> {
t.set(
"obj",
create_function_plain_errors(lua, |lua, args: MultiValue| {
let args = args.into_vec();
with_engine(lua, |e| e.pdf.output_opened = true)?;
match args.first() {
Some(Value::Table(t)) if args.len() == 1 => table_obj(lua, t),
_ => positional_obj(lua, &args, false),
}
})?,
)?;
t.set(
"immediateobj",
create_function_plain_errors(lua, |lua, args: MultiValue| {
positional_obj(lua, &args.into_vec(), true)
})?,
)?;
t.set(
"reserveobj",
create_function_plain_errors(lua, |lua, args: MultiValue| {
let annot = match args.len() {
0 => false,
1 => match args.front() {
Some(Value::String(s)) if s.as_bytes().as_ref() == b"annot" => true,
Some(Value::String(_)) => {
return err("pdf.reserveobj() optional string must be \"annot\"");
}
_ => return err("pdf.reserveobj() optional argument must be string"),
},
_ => return err("pdf.reserveobj() allows max. 1 argument"),
};
with_engine(lua, |e| {
if annot {
let n = e.pdf_alloc_obj(ObjKind::Annot);
e.pdf.last_annot = n;
i64::from(n)
} else {
let n = e.pdf_alloc_obj(ObjKind::Obj);
e.pdf.objects.insert(
n,
PdfObject {
num: n,
..Default::default()
},
);
e.pdf.last_obj = n;
i64::from(n)
}
})
})?,
)?;
t.set(
"refobj",
create_function_plain_errors(lua, |lua, args: MultiValue| {
if args.len() != 1 {
return err("pdf.refobj() needs exactly 1 argument");
}
let n = check_integer(lua, args.front(), 1, "pdf.refobj")?;
let n = i32::try_from(n).unwrap_or(0);
with_engine(lua, |e| {
if e.pdf.late.is_some() {
e.pdf_write_object(n);
} else {
e.pdf_append_refobj(n);
}
})
})?,
)?;
t.set(
"registerannot",
create_function_plain_errors(lua, |lua, args: MultiValue| {
if args.len() != 1 {
return err("pdf.registerannot() needs exactly 1 argument");
}
let n = check_integer(lua, args.front(), 1, "pdf.registerannot")?;
let n = i32::try_from(n).unwrap_or(0);
with_engine(lua, |e| match e.pdf.late.as_mut() {
Some(ctx) => {
ctx.annots.push(n);
Ok(())
}
None => err("pdf.registerannot() can only be used in late lua"),
})?
})?,
)?;
t.set(
"newcolorstack",
create_function_plain_errors(lua, |lua, args: MultiValue| {
let Some(Value::String(init)) = args.front() else {
return err("pdf.newcolorstack() expects a string as first argument");
};
let init = init.as_bytes().to_vec();
let mut lmode = literal_mode::ORIGIN;
if let Some(Value::String(m)) = args.get(1) {
match MODES
.iter()
.find(|(n, _)| n.as_bytes() == m.as_bytes().as_ref())
{
Some(&(_, mode)) => lmode = mode,
None => return err("invalid literal mode in pdf.newcolorstack()"),
}
}
let page = matches!(args.get(2), Some(Value::Boolean(true)));
with_engine(lua, |e| i64::from(e.pdf_new_colorstack(init, page, lmode)))
})?,
)?;
Ok(())
}
fn install_output(lua: &Lua, t: &Table) -> mlua::Result<()> {
t.set(
"print",
create_function_plain_errors(lua, |lua, args: MultiValue| {
let args = args.into_vec();
let (mode, text) = match args.as_slice() {
[s] => (literal_mode::ORIGIN, s),
[Value::String(m), s] => {
match MODES
.iter()
.find(|(n, _)| n.as_bytes() == m.as_bytes().as_ref())
{
Some(&(_, mode)) => (mode, s),
None => return err("invalid first argument for print literal mode"),
}
}
[_, _] => return err("invalid first argument for print literal mode"),
[] => return err("no string to print"),
_ => return err("invalid number of arguments"),
};
let Some(text) = coerce_bytes(lua, text)? else {
return err("no string to print");
};
with_engine(lua, |e| {
if let Some(ctx) = e.pdf.late.as_mut() {
ctx.prints.push((mode, text));
}
})
})?,
)?;
for name in ["mapfile", "mapline"] {
t.set(
name,
create_function_plain_errors(lua, |lua, args: MultiValue| {
let Some(Value::String(s)) = args.front() else {
return Ok(());
};
let s = s.as_bytes().to_vec();
with_engine(lua, |e| e.pdf.map_entries.push(s))
})?,
)?;
}
t.set(
"setfontattributes",
create_function_plain_errors(lua, |lua, args: MultiValue| {
let f = check_integer(lua, args.front(), 1, "pdf.setfontattributes")?;
let Some(s) = args
.get(1)
.map(|v| coerce_bytes(lua, v))
.transpose()?
.flatten()
else {
return err(format!(
"bad argument #2 to 'pdf.setfontattributes' (string expected, got {})",
args.get(1).map_or("no value", Value::type_name)
));
};
let f = u32::try_from(f).unwrap_or(0);
with_engine(lua, |e| {
e.pdf.font_attrs.insert(f, s);
})
})?,
)?;
for name in [
"includechar",
"includefont",
"setforcefile",
"settypeonewidemode",
] {
t.set(
name,
create_function_plain_errors(lua, |_, _: MultiValue| Ok(()))?,
)?;
}
t.set(
"includeimage",
create_function_plain_errors(lua, |_, _: MultiValue| -> mlua::Result<()> {
err("pdf.includeimage(): images are not supported yet")
})?,
)?;
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn number_conversions() {
assert_eq!(int_value(&Value::Integer(7)), Some(7));
assert_eq!(int_value(&Value::Number(3.0)), Some(3));
assert_eq!(int_value(&Value::Number(2.7)), Some(0));
assert_eq!(int_value(&Value::Number(1e300)), Some(0));
assert_eq!(int_value(&Value::Number(f64::NAN)), Some(0));
assert_eq!(int_value(&Value::Integer(4_294_967_301)), Some(5));
assert_eq!(int_value(&Value::Integer(2_147_483_648)), Some(i32::MIN));
assert_eq!(int_value(&Value::Boolean(true)), None);
assert_eq!(dimen_value(&Value::Number(2.5)), Some(3));
assert_eq!(dimen_value(&Value::Number(-2.5)), Some(-2));
assert_eq!(dimen_value(&Value::Number(-1.5)), Some(-1));
assert_eq!(dimen_value(&Value::Number(0.49)), Some(0));
assert_eq!(dimen_value(&Value::Integer(4_294_967_301)), Some(i32::MIN));
assert_eq!(dimen_value(&Value::Number(f64::NAN)), Some(i32::MIN));
assert_eq!(dimen_value(&Value::Nil), None);
}
}