pub mod base14;
pub mod cid;
pub mod fonts;
pub mod ttf;
pub use base14::Base14Metrics;
pub use cid::{render_pdf_ttf, render_pdf_ttf_with};
pub use fonts::{FontConfigError, FontFlags, FontRegistry, FontSource};
pub use ttf::{FontError, TtfFontStore};
use std::collections::{BTreeMap, BTreeSet};
use pdf_writer::types::{ActionType, AnnotationType};
use pdf_writer::{Content, Filter, Finish, Name, Pdf, Rect, Ref, Str, TextStr};
use rustyfi_backend::{
place_block_at, Annot, AnnotAction, Closing, Color, DocExtras, DocInfo, GraphicsElem,
ImageResource, Length, MathGlyph, NamedDest, ObjRepr, OutlineEntry, Page, PageGeometry, Path,
PathSeg, PureHorzBox, VertBox,
};
#[derive(Debug, thiserror::Error)]
pub enum PdfError {
#[error("text {0:?} is not encodable in WinAnsi (milestone-1 base fonts)")]
Unencodable(String),
#[error("no glyph for {0:?} in the embedded font")]
NoGlyph(char),
#[error(transparent)]
Io(#[from] std::io::Error),
}
const FONT_RES_NAMES: [&str; 3] = ["F0", "F1", "F2"];
fn used_images(pages: &[Page], overlays: &[Vec<GraphicsElem>]) -> BTreeSet<usize> {
let mut used = BTreeSet::new();
{
let mut note = |bx: &PureHorzBox| {
if let PureHorzBox::Image { image, .. } = bx {
used.insert(image.0);
}
};
for page in pages {
page.visit(&mut note);
}
for overlay in overlays {
for elem in overlay {
elem.visit(&mut note);
}
}
}
used
}
fn image_res_name(id: usize) -> String {
format!("Im{id}")
}
fn write_image_xobjects(
pdf: &mut Pdf,
mut next_ref: impl FnMut() -> Ref,
images: &[ImageResource],
used: &BTreeSet<usize>,
) -> BTreeMap<usize, Ref> {
let mut refs = BTreeMap::new();
for &id in used {
let Some(im) = images.get(id) else {
continue;
};
if im.pdf.is_some() {
continue;
}
let r = next_ref();
refs.insert(id, r);
if let Some(dct) = &im.jpeg_dct {
let mut xo = pdf.image_xobject(r, &dct.bytes);
xo.filter(Filter::DctDecode);
xo.width(im.px_w as i32);
xo.height(im.px_h as i32);
if dct.components == 1 {
xo.color_space().device_gray();
} else {
xo.color_space().device_rgb();
}
xo.bits_per_component(8);
xo.finish();
} else {
let mut xo = pdf.image_xobject(r, &im.samples);
xo.width(im.px_w as i32);
xo.height(im.px_h as i32);
xo.color_space().device_rgb();
xo.bits_per_component(8);
xo.finish();
}
}
refs
}
fn form_res_name(id: usize) -> String {
format!("Fm{id}")
}
fn write_pdf_obj_value(obj: pdf_writer::Obj<'_>, repr: &ObjRepr, remap: &BTreeMap<u32, Ref>) {
match repr {
ObjRepr::Null => obj.primitive(pdf_writer::Null),
ObjRepr::Bool(b) => obj.primitive(*b),
ObjRepr::Int(n) => obj.primitive(*n as i32),
ObjRepr::Real(r) => obj.primitive(*r as f32),
ObjRepr::Name(n) => obj.primitive(Name(n)),
ObjRepr::String(s) => obj.primitive(Str(s)),
ObjRepr::Ref(local_id) => match remap.get(local_id) {
Some(r) => obj.primitive(*r),
None => obj.primitive(pdf_writer::Null),
},
ObjRepr::Array(items) => {
let mut arr = obj.array();
for item in items {
write_pdf_obj_value(arr.push(), item, remap);
}
arr.finish();
}
ObjRepr::Dict(entries) => {
let mut dict = obj.dict();
for (k, v) in entries {
write_pdf_obj_value(dict.insert(Name(k)), v, remap);
}
dict.finish();
}
ObjRepr::Stream(..) => obj.primitive(pdf_writer::Null),
}
}
fn write_pdf_obj(pdf: &mut Pdf, out_ref: Ref, repr: &ObjRepr, remap: &BTreeMap<u32, Ref>) {
match repr {
ObjRepr::Stream(entries, bytes) => {
let mut stream = pdf.stream(out_ref, bytes);
for (k, v) in entries {
write_pdf_obj_value(stream.insert(Name(k)), v, remap);
}
stream.finish();
}
other => write_pdf_obj_value(pdf.indirect(out_ref), other, remap),
}
}
fn write_form_xobjects(
pdf: &mut Pdf,
mut next_ref: impl FnMut() -> Ref,
images: &[ImageResource],
used: &BTreeSet<usize>,
) -> BTreeMap<usize, Ref> {
let mut refs = BTreeMap::new();
for &id in used {
let Some(im) = images.get(id) else { continue };
let Some(pdf_res) = &im.pdf else { continue };
let mut remap: BTreeMap<u32, Ref> = BTreeMap::new();
let mut root_repr: Option<&ObjRepr> = None;
for (local_id, repr) in &pdf_res.resources.0 {
if *local_id == 0 {
root_repr = Some(repr);
} else {
remap.entry(*local_id).or_insert_with(&mut next_ref);
}
}
for (local_id, repr) in &pdf_res.resources.0 {
if *local_id != 0 {
write_pdf_obj(pdf, remap[local_id], repr, &remap);
}
}
let form_ref = next_ref();
let (x0, y0, x1, y1) = pdf_res.media_box;
{
let mut fx = pdf.form_xobject(form_ref, &pdf_res.content);
fx.bbox(Rect::new(x0 as f32, y0 as f32, x1 as f32, y1 as f32));
fx.matrix([1.0, 0.0, 0.0, 1.0, 0.0, 0.0]);
if let Some(root) = root_repr {
write_pdf_obj_value(fx.insert(Name(b"Resources")), root, &remap);
}
fx.finish();
}
refs.insert(id, form_ref);
}
refs
}
fn place_image(content: &mut Content, id: usize, tx: f32, ty: f32, width: f32, height: f32) {
content.save_state();
content.transform([width, 0.0, 0.0, height, tx, ty]);
content.x_object(Name(image_res_name(id).as_bytes()));
content.restore_state();
}
fn place_form(
content: &mut Content,
id: usize,
tx: f32,
ty: f32,
width: f32,
height: f32,
media_box: (f64, f64, f64, f64),
) {
let (x0, y0, x1, y1) = media_box;
let bbox_w = (x1 - x0) as f32;
let bbox_h = (y1 - y0) as f32;
let sx = if bbox_w != 0.0 { width / bbox_w } else { 1.0 };
let sy = if bbox_h != 0.0 { height / bbox_h } else { 1.0 };
content.save_state();
content.transform([
sx,
0.0,
0.0,
sy,
tx - sx * x0 as f32,
ty - sy * y0 as f32,
]);
content.x_object(Name(form_res_name(id).as_bytes()));
content.restore_state();
}
pub(crate) fn place_math(
content: &mut Content,
glyphs: &[MathGlyph],
anchor_x: f32,
anchor_y: f32,
name_for: &dyn Fn(rustyfi_backend::FontKey) -> String,
mut encode: impl FnMut(&MathGlyph) -> Result<Vec<u8>, PdfError>,
) -> Result<(), PdfError> {
for glyph in glyphs {
let encoded = encode(glyph)?;
let res_name = name_for(glyph.info.font);
let colored = glyph.info.color != Color::Gray(0.0);
if colored {
content.save_state();
set_fill_color(content, glyph.info.color);
}
content.begin_text();
content.set_font(Name(res_name.as_bytes()), glyph.info.size.0 as f32);
content.next_line(
anchor_x + glyph.dx.0 as f32,
anchor_y + glyph.dy.0 as f32 + glyph.info.rising.0 as f32,
);
content.show(Str(&encoded));
content.end_text();
if colored {
content.restore_state();
}
}
Ok(())
}
pub(crate) fn place_embedded_block(
block: &[VertBox],
tx: f32,
ty: f32,
anchor_last: bool,
mut emit_line: impl FnMut(&PureHorzBox, f32, f32) -> Result<(), PdfError>,
) -> Result<(), PdfError> {
let placed = place_block_at((Length::ZERO, Length::ZERO), block.to_vec());
let anchor = if anchor_last { placed.last() } else { placed.first() };
let Some(anchor) = anchor else {
return Ok(());
};
let anchor_offset = anchor.baseline_y;
for line in &placed {
let y = ty - (line.baseline_y - anchor_offset).0 as f32;
for (dx, cbx) in &line.contents {
emit_line(cbx, tx + (line.x + *dx).0 as f32, y)?;
}
}
Ok(())
}
pub(crate) fn write_annotations(
pdf: &mut Pdf,
mut next_ref: impl FnMut() -> Ref,
annots: &[Annot],
n_pages: usize,
) -> BTreeMap<usize, Vec<Ref>> {
let mut by_page: BTreeMap<usize, Vec<Ref>> = BTreeMap::new();
for a in annots {
if a.page >= n_pages {
continue; }
let r = next_ref();
let mut ann = pdf.annotation(r);
ann.subtype(AnnotationType::Link);
let (x1, y1, x2, y2) = a.rect;
ann.rect(Rect::new(x1.0 as f32, y1.0 as f32, x2.0 as f32, y2.0 as f32));
let width = a.border.as_ref().map(|(w, _)| w.0 as f32).unwrap_or(0.0);
ann.border(0.0, 0.0, width, None);
if let Some((_, color)) = &a.border {
match *color {
Color::Gray(g) => {
ann.color_gray(g as f32);
}
Color::Rgb(r, g, b) => {
ann.color_rgb(r as f32, g as f32, b as f32);
}
Color::Cmyk(c, m, y, k) => {
ann.color_cmyk(c as f32, m as f32, y as f32, k as f32);
}
}
}
let mut act = ann.action();
match &a.action {
AnnotAction::Uri(uri) => {
act.action_type(ActionType::Uri);
act.uri(Str(uri.as_bytes()));
}
AnnotAction::GotoName(name) => {
act.action_type(ActionType::GoTo);
act.destination_named(Name(name.as_bytes()));
}
}
act.finish();
ann.finish();
by_page.entry(a.page).or_default().push(r);
}
by_page
}
pub(crate) fn write_named_dests(
pdf: &mut Pdf,
mut next_ref: impl FnMut() -> Ref,
dests: &[NamedDest],
page_ids: &[Ref],
) -> Option<Ref> {
let mut dedup: BTreeMap<&str, &NamedDest> = BTreeMap::new();
for d in dests {
if d.page < page_ids.len() {
dedup.insert(d.name.as_str(), d);
}
}
if dedup.is_empty() {
return None;
}
let id = next_ref();
let mut dict = pdf.destinations(id); for (name, d) in dedup {
dict.insert(Name(name.as_bytes()))
.page(page_ids[d.page])
.xyz(d.x.0 as f32, d.y.0 as f32, None);
}
dict.finish();
Some(id)
}
pub(crate) fn write_outline(
pdf: &mut Pdf,
mut next_ref: impl FnMut() -> Ref,
entries: &[OutlineEntry],
) -> Option<Ref> {
if entries.is_empty() {
return None;
}
let root_id = next_ref();
let ids: Vec<Ref> = entries.iter().map(|_| next_ref()).collect();
let mut parent: Vec<Option<usize>> = vec![None; entries.len()];
let mut stack: Vec<usize> = Vec::new(); for i in 0..entries.len() {
while let Some(&top) = stack.last() {
if entries[top].level < entries[i].level {
break;
}
stack.pop();
}
parent[i] = stack.last().copied();
stack.push(i);
}
let mut children: Vec<Vec<usize>> = vec![Vec::new(); entries.len()];
let mut top_level: Vec<usize> = Vec::new();
for i in 0..entries.len() {
match parent[i] {
Some(p) => children[p].push(i),
None => top_level.push(i),
}
}
fn descendants(children: &[Vec<usize>], i: usize) -> i32 {
children[i].iter().map(|&c| 1 + descendants(children, c)).sum()
}
{
let mut root = pdf.outline(root_id);
root.first(ids[*top_level.first().unwrap()]);
root.last(ids[*top_level.last().unwrap()]);
root.count(top_level.len() as i32);
}
for (i, e) in entries.iter().enumerate() {
let mut item = pdf.outline_item(ids[i]);
item.title(TextStr(&e.text));
item.parent(parent[i].map(|p| ids[p]).unwrap_or(root_id));
let sibs: &Vec<usize> = match parent[i] {
Some(p) => &children[p],
None => &top_level,
};
let pos = sibs.iter().position(|&x| x == i).unwrap();
if pos > 0 {
item.prev(ids[sibs[pos - 1]]);
}
if pos + 1 < sibs.len() {
item.next(ids[sibs[pos + 1]]);
}
if let (Some(&f), Some(&l)) = (children[i].first(), children[i].last()) {
item.first(ids[f]);
item.last(ids[l]);
let n = descendants(&children, i);
item.count(if e.is_open { n } else { -n });
}
item.dest_name(Name(e.dest_name.as_bytes()));
}
Some(root_id)
}
pub(crate) fn write_document_info(pdf: &mut Pdf, id: Ref, info: &DocInfo) {
let mut w = pdf.document_info(id);
if let Some(title) = &info.title {
w.title(TextStr(title));
}
if let Some(subject) = &info.subject {
w.subject(TextStr(subject));
}
if let Some(author) = &info.author {
w.author(TextStr(author));
}
if !info.keywords.is_empty() {
let joined = info.keywords.join(" ");
w.keywords(TextStr(&joined));
}
w.creator(TextStr("SATySFi"));
w.producer(TextStr("SATySFi"));
}
pub fn render_pdf(
geometry: &PageGeometry,
pages: &[Page],
images: &[ImageResource],
) -> Result<Vec<u8>, PdfError> {
render_pdf_with(geometry, pages, images, &DocExtras::default())
}
pub fn render_pdf_with(
geometry: &PageGeometry,
pages: &[Page],
images: &[ImageResource],
extras: &DocExtras,
) -> Result<Vec<u8>, PdfError> {
let mut pdf = Pdf::new();
let mut alloc = 1;
let mut next_ref = || {
let r = Ref::new(alloc);
alloc += 1;
r
};
let catalog_id = next_ref();
let page_tree_id = next_ref();
let font_ids: Vec<Ref> = (0..3).map(|_| next_ref()).collect();
let used = used_images(pages, &extras.page_graphics);
let img_refs = write_image_xobjects(&mut pdf, &mut next_ref, images, &used);
let form_refs = write_form_xobjects(&mut pdf, &mut next_ref, images, &used);
let page_ids: Vec<Ref> = pages.iter().map(|_| next_ref()).collect();
let content_ids: Vec<Ref> = pages.iter().map(|_| next_ref()).collect();
let annot_refs = write_annotations(&mut pdf, &mut next_ref, &extras.annotations, pages.len());
let dests_id = write_named_dests(&mut pdf, &mut next_ref, &extras.destinations, &page_ids);
let outline_id = write_outline(&mut pdf, &mut next_ref, &extras.outline);
if let Some(info) = &extras.doc_info {
let info_id = next_ref();
write_document_info(&mut pdf, info_id, info);
}
{
let mut cat = pdf.catalog(catalog_id);
cat.pages(page_tree_id);
if let Some(d) = dests_id {
cat.destinations(d); }
if let Some(o) = outline_id {
cat.outlines(o); }
}
{
let mut tree = pdf.pages(page_tree_id);
tree.kids(page_ids.iter().copied());
tree.count(page_ids.len() as i32);
}
for (i, name) in base14::BASE_FONT_NAMES.iter().enumerate() {
let mut font = pdf.type1_font(font_ids[i]);
font.base_font(Name(name.as_bytes()));
font.encoding_predefined(Name(b"WinAnsiEncoding"));
}
let paper_h = geometry.paper_height.0 as f32;
let media_box = Rect::new(0.0, 0.0, geometry.paper_width.0 as f32, paper_h);
for (i, ((page, &page_id), &content_id)) in
pages.iter().zip(&page_ids).zip(&content_ids).enumerate()
{
let overlay = extras.page_graphics.get(i).map(|v| v.as_slice()).unwrap_or(&[]);
let content = page_content(page, paper_h, overlay, images)?;
pdf.stream(content_id, &content);
let mut p = pdf.page(page_id);
p.media_box(media_box);
p.parent(page_tree_id);
p.contents(content_id);
if let Some(refs) = annot_refs.get(&i) {
p.annotations(refs.iter().copied()); }
let mut resources = p.resources();
let mut fonts = resources.fonts();
for (i, res_name) in FONT_RES_NAMES.iter().enumerate() {
fonts.pair(Name(res_name.as_bytes()), font_ids[i]);
}
fonts.finish();
if !img_refs.is_empty() || !form_refs.is_empty() {
let mut x_objects = resources.x_objects();
for (&id, &r) in &img_refs {
x_objects.pair(Name(image_res_name(id).as_bytes()), r);
}
for (&id, &r) in &form_refs {
x_objects.pair(Name(form_res_name(id).as_bytes()), r);
}
x_objects.finish();
}
resources.finish();
p.finish();
}
Ok(pdf.finish())
}
fn page_content(
page: &Page,
paper_h: f32,
overlay: &[GraphicsElem],
images: &[ImageResource],
) -> Result<Vec<u8>, PdfError> {
let mut content = Content::new();
if !overlay.is_empty() {
place_graphics(&mut content, overlay, 0.0, 0.0, &mut |c, bx, x, y| {
emit_box(c, bx, x, y, images)
})?;
}
for line in &page.lines {
let y = paper_h - line.baseline_y.0 as f32;
for (dx, bx) in &line.contents {
emit_box(&mut content, bx, (line.x + *dx).0 as f32, y, images)?;
}
}
Ok(content.finish().into_vec())
}
fn emit_box(
content: &mut Content,
bx: &PureHorzBox,
tx: f32,
ty: f32,
images: &[ImageResource],
) -> Result<(), PdfError> {
match bx {
PureHorzBox::InnerString { info, text, .. } => {
let encoded = winansi(text)?;
let font_idx = (info.font.0 as usize).min(FONT_RES_NAMES.len() - 1);
let colored = info.color != Color::Gray(0.0);
if colored {
content.save_state();
set_fill_color(content, info.color);
}
content.begin_text();
content.set_font(
Name(FONT_RES_NAMES[font_idx].as_bytes()),
info.size.0 as f32,
);
content.next_line(tx, ty + info.rising.0 as f32);
content.show(Str(&encoded));
content.end_text();
if colored {
content.restore_state();
}
}
PureHorzBox::Image {
width,
height,
image,
} => {
match images.get(image.0).and_then(|im| im.pdf.as_ref()) {
Some(pdf_res) => place_form(
content,
image.0,
tx,
ty,
width.0 as f32,
height.0 as f32,
pdf_res.media_box,
),
None => place_image(content, image.0, tx, ty, width.0 as f32, height.0 as f32),
}
}
PureHorzBox::Graphics { elems, origin_independent, .. } => {
let (ax, ay) = if *origin_independent { (0.0, 0.0) } else { (tx, ty) };
place_graphics(content, elems, ax, ay, &mut |c, bx, x, y| {
emit_box(c, bx, x, y, images)
})?;
}
PureHorzBox::Math { glyphs, rules, .. } => {
let name_for = |k: rustyfi_backend::FontKey| {
FONT_RES_NAMES[(k.0 as usize).min(FONT_RES_NAMES.len() - 1)].to_string()
};
place_math(content, glyphs, tx, ty, &name_for, |g| winansi(&g.text))?;
place_graphics(content, rules, tx, ty, &mut |c, bx, x, y| {
emit_box(c, bx, x, y, images)
})?;
}
PureHorzBox::Tabular(tab) => {
for cell in &tab.cells {
for (cdx, cbx) in &cell.contents {
emit_box(
content,
cbx,
tx + (cell.x + *cdx).0 as f32,
ty + cell.baseline_y.0 as f32,
images,
)?;
}
}
place_graphics(content, &tab.rules, tx, ty, &mut |c, bx, x, y| {
emit_box(c, bx, x, y, images)
})?;
}
PureHorzBox::EmbeddedBlock { block, anchor_last, .. } => {
place_embedded_block(block, tx, ty, *anchor_last, |cbx, x, y| {
emit_box(content, cbx, x, y, images)
})?;
}
PureHorzBox::Frame { contents, .. } => {
for (dx, cbx) in contents {
emit_box(content, cbx, tx + dx.0 as f32, ty, images)?;
}
}
_ => {}
}
Ok(())
}
pub(crate) type NestedEmitter<'a> =
&'a mut dyn FnMut(&mut Content, &PureHorzBox, f32, f32) -> Result<(), PdfError>;
pub(crate) fn place_graphics(
content: &mut Content,
elems: &[GraphicsElem],
tx: f32,
ty: f32,
emit_nested: NestedEmitter<'_>,
) -> Result<(), PdfError> {
content.save_state();
content.transform([1.0, 0.0, 0.0, 1.0, tx, ty]);
for elem in elems {
if matches!(elem, GraphicsElem::Destination { .. }) {
continue;
}
content.save_state();
match elem {
GraphicsElem::Fill(color, path) => {
set_fill_color(content, *color);
emit_path(content, path);
content.fill_even_odd();
}
GraphicsElem::Stroke(width, color, path) => {
set_stroke_color(content, *color);
content.set_line_width(width.0 as f32);
emit_path(content, path);
content.stroke();
}
GraphicsElem::DashedStroke(width, dash, color, path) => {
set_stroke_color(content, *color);
content.set_line_width(width.0 as f32);
content.set_dash_pattern([dash.0 .0 as f32, dash.1 .0 as f32], dash.2 .0 as f32);
emit_path(content, path);
content.stroke();
}
GraphicsElem::Text { pt, contents, transform, .. } => {
match transform {
None => {
for (dx, bx) in contents {
emit_nested(content, bx, (pt.0 + *dx).0 as f32, pt.1 .0 as f32)?;
}
}
Some((a, b, c, d)) => {
content.transform([
*a as f32,
*c as f32,
*b as f32,
*d as f32,
pt.0 .0 as f32,
pt.1 .0 as f32,
]);
for (dx, bx) in contents {
emit_nested(content, bx, (*dx).0 as f32, 0.0)?;
}
}
}
}
GraphicsElem::Group(inner) => {
place_graphics(content, inner, 0.0, 0.0, &mut *emit_nested)?;
}
GraphicsElem::Clip(path, inner) => {
emit_path(content, path);
content.clip_even_odd();
content.end_path();
place_graphics(content, inner, 0.0, 0.0, &mut *emit_nested)?;
}
GraphicsElem::Destination { .. } => {}
}
content.restore_state();
}
content.restore_state();
Ok(())
}
pub(crate) fn set_fill_color(content: &mut Content, color: Color) {
match color {
Color::Gray(g) => content.set_fill_gray(g as f32),
Color::Rgb(r, g, b) => content.set_fill_rgb(r as f32, g as f32, b as f32),
Color::Cmyk(c, m, y, k) => content.set_fill_cmyk(c as f32, m as f32, y as f32, k as f32),
};
}
fn set_stroke_color(content: &mut Content, color: Color) {
match color {
Color::Gray(g) => content.set_stroke_gray(g as f32),
Color::Rgb(r, g, b) => content.set_stroke_rgb(r as f32, g as f32, b as f32),
Color::Cmyk(c, m, y, k) => {
content.set_stroke_cmyk(c as f32, m as f32, y as f32, k as f32)
}
};
}
fn emit_path(content: &mut Content, path: &Path) {
for sub in &path.subpaths {
content.move_to(sub.start.0 .0 as f32, sub.start.1 .0 as f32);
for seg in &sub.segs {
match seg {
PathSeg::Line(pt) => {
content.line_to(pt.0 .0 as f32, pt.1 .0 as f32);
}
PathSeg::Bezier(c1, c2, dest) => {
content.cubic_to(
c1.0 .0 as f32,
c1.1 .0 as f32,
c2.0 .0 as f32,
c2.1 .0 as f32,
dest.0 .0 as f32,
dest.1 .0 as f32,
);
}
}
}
match sub.closing {
Closing::Open => {}
Closing::Line => {
content.close_path();
}
Closing::Bezier(c1, c2) => {
content.cubic_to(
c1.0 .0 as f32,
c1.1 .0 as f32,
c2.0 .0 as f32,
c2.1 .0 as f32,
sub.start.0 .0 as f32,
sub.start.1 .0 as f32,
);
content.close_path();
}
}
}
}
fn winansi(text: &str) -> Result<Vec<u8>, PdfError> {
let mut out = Vec::with_capacity(text.len());
for c in text.chars() {
let code = c as u32;
if (32..=126).contains(&code) {
out.push(code as u8);
} else {
return Err(PdfError::Unencodable(text.to_string()));
}
}
Ok(out)
}