use lopdf::{dictionary, Document, Object, Stream};
#[cfg(feature = "fonts")]
pub(crate) fn be16(v: u16) -> [u8; 2] {
v.to_be_bytes()
}
#[cfg(feature = "fonts")]
pub(crate) fn be32(v: u32) -> [u8; 4] {
v.to_be_bytes()
}
pub(crate) struct PageFixture {
pub content: Stream,
pub helvetica_f1: bool,
pub page_extras: Vec<(&'static str, Object)>,
pub media_box: Option<[i64; 4]>,
pub pages_media_box: Option<[i64; 4]>,
}
impl PageFixture {
pub(crate) fn for_content(content_ops: &[u8]) -> Self {
Self {
content: Stream::new(dictionary! {}, content_ops.to_vec()),
helvetica_f1: true,
page_extras: Vec::new(),
media_box: Some([0, 0, 612, 792]),
pages_media_box: None,
}
}
}
pub(crate) fn one_page_pdf(fx: PageFixture) -> Document {
let mut doc = Document::with_version("1.7");
let pages_id = doc.new_object_id();
let mut font_resources = dictionary! {};
if fx.helvetica_f1 {
let font_id = doc.add_object(dictionary! {
"Type" => "Font",
"Subtype" => "Type1",
"BaseFont" => "Helvetica",
"Encoding" => "WinAnsiEncoding",
});
font_resources = dictionary! { "F1" => font_id };
}
let resources_id = doc.add_object(dictionary! { "Font" => font_resources });
let content_id = doc.add_object(fx.content);
let mut page_dict = dictionary! {
"Type" => "Page",
"Parent" => pages_id,
"Contents" => content_id,
"Resources" => resources_id,
};
if let Some([a, b, c, d]) = fx.media_box {
page_dict.set(
"MediaBox",
Object::Array(vec![
Object::Integer(a),
Object::Integer(b),
Object::Integer(c),
Object::Integer(d),
]),
);
}
for (key, value) in fx.page_extras {
page_dict.set(key, value);
}
let page_id = doc.add_object(page_dict);
let mut pages_dict = dictionary! {
"Type" => "Pages",
"Kids" => Object::Array(vec![Object::Reference(page_id)]),
"Count" => 1,
};
if let Some([a, b, c, d]) = fx.pages_media_box {
pages_dict.set(
"MediaBox",
Object::Array(vec![
Object::Integer(a),
Object::Integer(b),
Object::Integer(c),
Object::Integer(d),
]),
);
}
doc.objects.insert(pages_id, Object::Dictionary(pages_dict));
let catalog_id = doc.add_object(dictionary! {
"Type" => "Catalog",
"Pages" => pages_id,
});
doc.trailer.set("Root", catalog_id);
doc
}
pub(crate) fn custom_page_pdf(
fonts: impl FnOnce(&mut Document) -> Vec<(&'static str, lopdf::ObjectId)>,
streams: &[&[u8]],
indirect_array: bool,
) -> Document {
let mut doc = Document::with_version("1.7");
let pages_id = doc.new_object_id();
let mut font_resources = dictionary! {};
for (name, id) in fonts(&mut doc) {
font_resources.set(name, id);
}
let resources_id = doc.add_object(dictionary! { "Font" => font_resources });
let stream_ids: Vec<lopdf::ObjectId> = streams
.iter()
.map(|bytes| doc.add_object(Stream::new(dictionary! {}, bytes.to_vec())))
.collect();
let refs = Object::Array(stream_ids.iter().map(|id| Object::Reference(*id)).collect());
let contents: Object = if indirect_array {
Object::Reference(doc.add_object(refs))
} else if stream_ids.len() == 1 {
Object::Reference(stream_ids[0])
} else {
refs
};
let page_id = doc.add_object(dictionary! {
"Type" => "Page",
"Parent" => pages_id,
"Contents" => contents,
"Resources" => resources_id,
"MediaBox" => Object::Array(vec![
Object::Integer(0),
Object::Integer(0),
Object::Integer(612),
Object::Integer(792),
]),
});
doc.objects.insert(
pages_id,
Object::Dictionary(dictionary! {
"Type" => "Pages",
"Kids" => Object::Array(vec![Object::Reference(page_id)]),
"Count" => 1,
}),
);
let catalog_id = doc.add_object(dictionary! { "Type" => "Catalog", "Pages" => pages_id });
doc.trailer.set("Root", catalog_id);
doc
}
pub(crate) fn helvetica_font(doc: &mut Document) -> lopdf::ObjectId {
doc.add_object(dictionary! {
"Type" => "Font",
"Subtype" => "Type1",
"BaseFont" => "Helvetica",
"Encoding" => "WinAnsiEncoding",
})
}
pub(crate) fn subset_type1_font(
doc: &mut Document,
first_char: i64,
widths: &[i64],
encoding: Option<Object>,
to_unicode: Option<&str>,
) -> lopdf::ObjectId {
let program = doc.add_object(Stream::new(
dictionary! {},
b"not a real font program".to_vec(),
));
let descriptor = doc.add_object(dictionary! {
"Type" => "FontDescriptor",
"FontName" => "ABCDEF+Subset",
"Flags" => 4,
"FontFile" => program,
});
let mut font = dictionary! {
"Type" => "Font",
"Subtype" => "Type1",
"BaseFont" => "ABCDEF+Subset",
"FirstChar" => first_char,
"LastChar" => first_char + widths.len() as i64 - 1,
"Widths" => Object::Array(widths.iter().map(|w| Object::Integer(*w)).collect()),
"FontDescriptor" => descriptor,
};
if let Some(encoding) = encoding {
font.set("Encoding", encoding);
}
if let Some(cmap) = to_unicode {
let id = doc.add_object(Stream::new(dictionary! {}, cmap.as_bytes().to_vec()));
font.set("ToUnicode", id);
}
doc.add_object(font)
}
pub(crate) fn differences_encoding(
base: Option<&str>,
typed: bool,
differences: Vec<Object>,
) -> Object {
let mut dict = dictionary! { "Differences" => Object::Array(differences) };
if typed {
dict.set("Type", "Encoding");
}
if let Some(base) = base {
dict.set("BaseEncoding", base);
}
Object::Dictionary(dict)
}
pub(crate) fn to_unicode_cmap(width: usize, pairs: &[(u32, char)]) -> String {
let mut out = String::from(
"/CIDInit /ProcSet findresource begin 12 dict begin begincmap\n\
1 begincodespacerange <00> <FF> endcodespacerange\n",
);
out.push_str(&format!("{} beginbfchar\n", pairs.len()));
for (code, ch) in pairs {
out.push_str(&format!(
"<{:0w$X}> <{:04X}>\n",
code,
*ch as u32,
w = width * 2
));
}
out.push_str("endbfchar\nendcmap end end\n");
out
}
pub(crate) fn strict_to_unicode_cmap(pairs: &[(u32, char)]) -> String {
let mut out = String::from(
"/CIDInit /ProcSet findresource begin\n\
12 dict begin\n\
begincmap\n\
/CIDSystemInfo << /Registry (Adobe) /Ordering (UCS) /Supplement 0 >> def\n\
/CMapName /Adobe-Identity-UCS def\n\
/CMapType 2 def\n\
1 begincodespacerange\n\
<00> <FF>\n\
endcodespacerange\n",
);
out.push_str(&format!("{} beginbfchar\n", pairs.len()));
for (code, ch) in pairs {
out.push_str(&format!("<{code:02X}> <{:04X}>\n", *ch as u32));
}
out.push_str("endbfchar\nendcmap\nCMapName currentdict /CMap defineresource pop\nend\nend\n");
out
}
pub(crate) fn identity_h_font(
doc: &mut Document,
widths: &[(u32, i64)],
mapped: &[(u32, char)],
) -> lopdf::ObjectId {
identity_h_font_with_cmap(doc, widths, to_unicode_cmap(2, mapped))
}
pub(crate) fn identity_h_font_with_cmap(
doc: &mut Document,
widths: &[(u32, i64)],
cmap_source: String,
) -> lopdf::ObjectId {
let mut w = Vec::new();
for (cid, width) in widths {
w.push(Object::Integer(*cid as i64));
w.push(Object::Array(vec![Object::Integer(*width)]));
}
let descendant = doc.add_object(dictionary! {
"Type" => "Font",
"Subtype" => "CIDFontType2",
"BaseFont" => "ABCDEF+Cid",
"CIDSystemInfo" => dictionary! {
"Registry" => Object::String(b"Adobe".to_vec(), lopdf::StringFormat::Literal),
"Ordering" => Object::String(b"Identity".to_vec(), lopdf::StringFormat::Literal),
"Supplement" => 0,
},
"DW" => 1000,
"W" => Object::Array(w),
"CIDToGIDMap" => "Identity",
});
let cmap = doc.add_object(Stream::new(dictionary! {}, cmap_source.into_bytes()));
doc.add_object(dictionary! {
"Type" => "Font",
"Subtype" => "Type0",
"BaseFont" => "ABCDEF+Cid",
"Encoding" => "Identity-H",
"DescendantFonts" => Object::Array(vec![Object::Reference(descendant)]),
"ToUnicode" => cmap,
})
}
pub(crate) fn raw_to_unicode_cmap(body: &str) -> String {
format!(
"/CIDInit /ProcSet findresource begin 12 dict begin begincmap\n\
1 begincodespacerange <0000> <FFFF> endcodespacerange\n{body}\nendcmap end end\n"
)
}
pub(crate) fn tex_style_font(doc: &mut Document, space_glyph: bool) -> lopdf::ObjectId {
let mut differences = Vec::new();
if space_glyph {
differences.push(Object::Integer(32));
differences.push(Object::Name(b"space".to_vec()));
}
differences.push(Object::Integer(97));
for letter in b'a'..=b'z' {
differences.push(Object::Name(vec![letter]));
}
let widths: Vec<i64> = (32i64..=122)
.map(|code| match code {
32 if space_glyph => 250,
97..=122 => 500,
_ => 0,
})
.collect();
let encoding = differences_encoding(None, true, differences);
subset_type1_font(doc, 32, &widths, Some(encoding), None)
}
pub(crate) fn code_32_letter_font(doc: &mut Document) -> lopdf::ObjectId {
let encoding = differences_encoding(
None,
true,
vec![
Object::Integer(32),
Object::Name(b"x".to_vec()),
Object::Integer(97),
Object::Name(b"a".to_vec()),
Object::Name(b"b".to_vec()),
],
);
let widths: Vec<i64> = (32i64..=98)
.map(|code| if matches!(code, 32 | 97 | 98) { 500 } else { 0 })
.collect();
subset_type1_font(doc, 32, &widths, Some(encoding), None)
}
pub(crate) fn code_5_space_font(doc: &mut Document) -> lopdf::ObjectId {
let encoding = differences_encoding(
None,
true,
vec![
Object::Integer(5),
Object::Name(b"space".to_vec()),
Object::Integer(97),
Object::Name(b"a".to_vec()),
Object::Name(b"b".to_vec()),
],
);
let widths: Vec<i64> = (5i64..=98)
.map(|code| match code {
5 => 250,
97 | 98 => 500,
_ => 0,
})
.collect();
subset_type1_font(doc, 5, &widths, Some(encoding), None)
}
pub(crate) fn inherited_resources_pdf(content: &[u8]) -> Document {
let mut doc = Document::with_version("1.7");
let pages_id = doc.new_object_id();
let font = helvetica_font(&mut doc);
let form = doc.add_object(Stream::new(
dictionary! {
"Type" => "XObject",
"Subtype" => "Form",
"BBox" => Object::Array(vec![
Object::Integer(0),
Object::Integer(0),
Object::Integer(10),
Object::Integer(10),
]),
},
Vec::new(),
));
let content_id = doc.add_object(Stream::new(dictionary! {}, content.to_vec()));
let page_id = doc.add_object(dictionary! {
"Type" => "Page",
"Parent" => pages_id,
"Contents" => content_id,
"MediaBox" => Object::Array(vec![
Object::Integer(0),
Object::Integer(0),
Object::Integer(612),
Object::Integer(792),
]),
});
doc.objects.insert(
pages_id,
Object::Dictionary(dictionary! {
"Type" => "Pages",
"Kids" => Object::Array(vec![Object::Reference(page_id)]),
"Count" => 1,
"Resources" => dictionary! {
"Font" => dictionary! { "F1" => font },
"XObject" => dictionary! { "Im1" => form },
},
}),
);
let catalog_id = doc.add_object(dictionary! { "Type" => "Catalog", "Pages" => pages_id });
doc.trailer.set("Root", catalog_id);
doc
}
pub(crate) fn tex_style_pdf(space_glyph: bool, content: &[u8]) -> Document {
custom_page_pdf(
|d| vec![("F1", tex_style_font(d, space_glyph))],
&[content],
false,
)
}
pub(crate) fn build_one_page_pdf(content_ops: &[u8]) -> Document {
one_page_pdf(PageFixture::for_content(content_ops))
}
#[cfg(feature = "edit")]
pub(crate) fn build_one_page_pdf_bytes(content_ops: &[u8]) -> Vec<u8> {
pdf_bytes(&build_one_page_pdf(content_ops))
}
#[cfg(feature = "edit")]
pub(crate) fn pdf_bytes(doc: &Document) -> Vec<u8> {
let mut doc = doc.clone();
let mut out = Vec::new();
doc.save_to(&mut out).expect("fixture PDF must save");
out
}
#[cfg(feature = "fonts")]
pub(crate) fn build_sfnt(tables: &[([u8; 4], Vec<u8>)]) -> Vec<u8> {
let mut tables = tables.to_vec();
tables.sort_by_key(|(tag, _)| *tag);
let tables = &tables[..];
let num_tables = tables.len() as u16;
let entry_selector = (num_tables as f64).log2().floor() as u16;
let search_range = 16u16.saturating_mul(1u16 << entry_selector.min(15));
let range_shift = (num_tables * 16).saturating_sub(search_range);
let header_len = 12 + tables.len() * 16;
let mut offset = header_len as u32;
let mut records = Vec::new();
let mut body = Vec::new();
for (tag, data) in tables {
records.extend(*tag);
records.extend(be32(0)); records.extend(be32(offset));
records.extend(be32(data.len() as u32));
body.extend_from_slice(data);
let padded_len = (data.len() + 3) & !3;
body.resize(body.len() + (padded_len - data.len()), 0);
offset += padded_len as u32;
}
let mut out = Vec::new();
out.extend(be32(0x0001_0000)); out.extend(be16(num_tables));
out.extend(be16(search_range));
out.extend(be16(entry_selector));
out.extend(be16(range_shift));
out.extend(records);
out.extend(body);
out
}
#[cfg(feature = "fonts")]
pub(crate) fn build_test_ttf(fs_type: u16, chars: &[(char, u16)], advance: u16) -> Vec<u8> {
let num_glyphs = chars.iter().map(|(_, g)| *g).max().unwrap_or(0) + 1;
let mut head = Vec::new();
head.extend(be16(1)); head.extend(be16(0)); head.extend(be32(0)); head.extend(be32(0)); head.extend(be32(0x5F0F3CF5)); head.extend(be16(0)); head.extend(be16(1000)); head.extend(be32(0)); head.extend(be32(0)); head.extend(be32(0)); head.extend(be32(0)); head.extend(be16(0)); head.extend(be16(0)); head.extend(be16(0)); head.extend(be16(0)); head.extend(be16(0)); head.extend(be16(8)); head.extend(be16(2)); head.extend(be16(0)); head.extend(be16(0)); assert_eq!(head.len(), 54);
let mut hhea = Vec::new();
hhea.extend(be16(1));
hhea.extend(be16(0));
hhea.extend(be16(800)); hhea.extend(be16((-200i16) as u16)); hhea.extend(be16(0)); hhea.extend(be16(advance)); hhea.extend(be16(0)); hhea.extend(be16(0)); hhea.extend(be16(advance)); hhea.extend(be16(1)); hhea.extend(be16(0)); hhea.extend(be16(0)); hhea.extend(be16(0)); hhea.extend(be16(0)); hhea.extend(be16(0)); hhea.extend(be16(0)); hhea.extend(be16(0)); hhea.extend(be16(num_glyphs)); assert_eq!(hhea.len(), 36);
let mut maxp = Vec::new();
maxp.extend(be32(0x0001_0000));
maxp.extend(be16(num_glyphs));
maxp.extend(be16(0)); maxp.extend(be16(0)); maxp.extend(be16(0)); maxp.extend(be16(0)); maxp.extend(be16(2)); maxp.extend(be16(0)); maxp.extend(be16(0)); maxp.extend(be16(0)); maxp.extend(be16(0)); maxp.extend(be16(0)); maxp.extend(be16(0)); maxp.extend(be16(0)); maxp.extend(be16(0)); assert_eq!(maxp.len(), 32);
let mut hmtx = Vec::new();
for _ in 0..num_glyphs {
hmtx.extend(be16(advance));
hmtx.extend(be16(0));
}
let mut loca = Vec::new();
for _ in 0..=num_glyphs {
loca.extend(be16(0));
}
let glyf: Vec<u8> = Vec::new();
let mut mappings: Vec<(u16, u16)> = chars.iter().map(|(c, g)| (*c as u16, *g)).collect();
mappings.sort_by_key(|(code, _)| *code);
let seg_count = mappings.len() + 1; let seg_count_x2 = (seg_count * 2) as u16;
let mut end_codes = Vec::new();
let mut start_codes = Vec::new();
let mut id_deltas = Vec::new();
let mut id_range_offsets = Vec::new();
for (code, gid) in &mappings {
end_codes.push(*code);
start_codes.push(*code);
id_deltas.push(gid.wrapping_sub(*code));
id_range_offsets.push(0u16);
}
end_codes.push(0xFFFF);
start_codes.push(0xFFFF);
id_deltas.push(1);
id_range_offsets.push(0);
let entry_selector = (seg_count as f64).log2().floor() as u16;
let search_range = 2u16.saturating_mul(1u16 << entry_selector.min(15));
let range_shift = seg_count_x2.saturating_sub(search_range);
let mut subtable = Vec::new();
subtable.extend(be16(4)); subtable.extend(be16(0)); subtable.extend(be16(0)); subtable.extend(be16(seg_count_x2));
subtable.extend(be16(search_range));
subtable.extend(be16(entry_selector));
subtable.extend(be16(range_shift));
for v in &end_codes {
subtable.extend(be16(*v));
}
subtable.extend(be16(0)); for v in &start_codes {
subtable.extend(be16(*v));
}
for v in &id_deltas {
subtable.extend(be16(*v));
}
for v in &id_range_offsets {
subtable.extend(be16(*v));
}
let length = subtable.len() as u16;
let length_bytes = be16(length);
subtable[2] = length_bytes[0];
subtable[3] = length_bytes[1];
let mut cmap = Vec::new();
cmap.extend(be16(0)); cmap.extend(be16(1)); cmap.extend(be16(3)); cmap.extend(be16(1)); cmap.extend(be32(12)); cmap.extend(subtable);
let mut os2 = Vec::new();
os2.extend(be16(2)); os2.extend(be16(0)); os2.extend(be16(400)); os2.extend(be16(5)); os2.extend(be16(fs_type)); os2.extend(be16(0)); os2.extend(be16(0)); os2.extend(be16(0)); os2.extend(be16(0)); os2.extend(be16(0)); os2.extend(be16(0)); os2.extend(be16(0)); os2.extend(be16(0)); os2.extend(be16(0)); os2.extend(be16(0)); os2.extend(be16(0)); os2.extend([0u8; 10]); os2.extend(be32(0)); os2.extend(be32(0)); os2.extend(be32(0)); os2.extend(be32(0)); os2.extend(*b"TEST"); os2.extend(be16(0x0040)); os2.extend(be16(0)); os2.extend(be16(0xFFFF)); os2.extend(be16(800)); os2.extend(be16((-200i16) as u16)); os2.extend(be16(0)); os2.extend(be16(1000)); os2.extend(be16(200)); os2.extend(be32(0)); os2.extend(be32(0)); os2.extend(be16(500)); os2.extend(be16(700)); os2.extend(be16(0)); os2.extend(be16(32)); os2.extend(be16(1)); assert_eq!(os2.len(), 96);
build_sfnt(&[
(*b"head", head),
(*b"hhea", hhea),
(*b"maxp", maxp),
(*b"hmtx", hmtx),
(*b"loca", loca),
(*b"glyf", glyf),
(*b"cmap", cmap),
(*b"OS/2", os2),
])
}