use std::fmt::Write as _;
use kurbo::{Affine, BezPath, PathEl, Point, Rect, RoundedRect, Shape};
use pdfrum_common::{Diagnostics, PageIndex};
use pdfrum_edit::{
ContentsShape, EmbeddedFont, EmbeddedImage, write_float, write_matrix, write_point,
};
use pdfrum_object::{
Array, ByteSpan, Dict, Name, ObjRef, Object, Resolve, Stream, names as pdf_names,
};
use crate::{Color, DocEdit, Error, Result};
#[derive(Debug, Clone, PartialEq)]
pub enum Paint {
Fill(Color),
Stroke(Stroke),
FillStroke(Color, Stroke),
}
impl Paint {
#[must_use]
pub fn fill(&self) -> Option<Color> {
match self {
Self::Fill(color) | Self::FillStroke(color, _) => Some(*color),
Self::Stroke(_) => None,
}
}
#[must_use]
pub fn stroke(&self) -> Option<&Stroke> {
match self {
Self::Stroke(stroke) | Self::FillStroke(_, stroke) => Some(stroke),
Self::Fill(_) => None,
}
}
}
#[derive(Debug, Clone, PartialEq)]
pub struct Stroke {
pub color: Color,
pub width: f64,
pub cap: LineCap,
pub join: LineJoin,
pub miter_limit: MiterLimit,
pub dash: Option<Dash>,
}
impl Stroke {
#[must_use]
pub fn new(color: Color, width: f64) -> Self {
Self {
color,
width,
cap: LineCap::Butt,
join: LineJoin::Miter,
miter_limit: MiterLimit::default(),
dash: None,
}
}
#[must_use]
pub fn with_cap(mut self, cap: LineCap) -> Self {
self.cap = cap;
self
}
#[must_use]
pub fn with_join(mut self, join: LineJoin) -> Self {
self.join = join;
self
}
#[must_use]
pub fn with_miter_limit(mut self, limit: MiterLimit) -> Self {
self.miter_limit = limit;
self
}
#[must_use]
pub fn with_dash(mut self, dash: Dash) -> Self {
self.dash = Some(dash);
self
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum LineCap {
#[default]
Butt,
Round,
Square,
}
impl LineCap {
fn operand(self) -> u8 {
match self {
Self::Butt => 0,
Self::Round => 1,
Self::Square => 2,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum LineJoin {
#[default]
Miter,
Round,
Bevel,
}
impl LineJoin {
fn operand(self) -> u8 {
match self {
Self::Miter => 0,
Self::Round => 1,
Self::Bevel => 2,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, PartialOrd)]
pub struct MiterLimit(f64);
impl MiterLimit {
#[must_use]
pub fn new(ratio: f64) -> Self {
if ratio.is_finite() {
Self(ratio.max(1.0))
} else {
Self::default()
}
}
#[must_use]
pub fn get(self) -> f64 {
self.0
}
}
impl Default for MiterLimit {
fn default() -> Self {
Self(10.0)
}
}
#[derive(Debug, Clone, PartialEq)]
pub struct Dash {
lengths: Vec<f64>,
phase: f64,
}
impl Dash {
#[must_use]
pub fn new(lengths: &[f64], phase: f64) -> Option<Self> {
if lengths.is_empty() || !phase.is_finite() || phase < 0.0 {
return None;
}
if lengths
.iter()
.any(|length| !length.is_finite() || *length < 0.0)
{
return None;
}
if lengths.iter().sum::<f64>() <= 0.0 {
return None;
}
Some(Self {
lengths: lengths.to_vec(),
phase,
})
}
#[must_use]
pub fn lengths(&self) -> &[f64] {
&self.lengths
}
#[must_use]
pub fn phase(&self) -> f64 {
self.phase
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum Fill {
#[default]
NonZero,
EvenOdd,
}
pub struct Canvas<'a, 'b> {
out: String,
edit: &'a mut DocEdit<'b>,
added: Vec<(&'static Name, Name, Object)>,
taken: Vec<(&'static Name, Name)>,
size: kurbo::Size,
surface: Surface,
failed: Option<Error>,
}
impl std::fmt::Debug for Canvas<'_, '_> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("Canvas")
.field("size", &self.size)
.field("surface", &self.surface)
.field("failed", &self.failed)
.finish_non_exhaustive()
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum Surface {
Page(PageIndex),
#[cfg(feature = "svg-import")]
Form,
}
impl Canvas<'_, '_> {
#[must_use]
pub fn size(&self) -> kurbo::Size {
self.size
}
#[must_use]
pub fn bounds(&self) -> Rect {
Rect::from_origin_size(Point::ZERO, self.size)
}
#[must_use]
pub fn page(&self) -> Option<PageIndex> {
match self.surface {
Surface::Page(index) => Some(index),
#[cfg(feature = "svg-import")]
Surface::Form => None,
}
}
pub fn saved(&mut self, body: impl FnOnce(&mut Self)) {
self.out.push_str("q\n");
body(self);
self.out.push_str("Q\n");
}
pub fn transform(&mut self, transform: Affine) {
write_matrix(&mut self.out, transform);
self.out.push_str(" cm\n");
}
pub fn clip(&mut self, shape: impl Shape, rule: Fill) {
self.write_path(&shape.into_path(0.1));
self.out.push_str(match rule {
Fill::NonZero => " W n\n",
Fill::EvenOdd => " W* n\n",
});
}
pub fn fill(&mut self, shape: impl Shape, color: Color) {
self.draw(shape, Paint::Fill(color), Fill::NonZero);
}
pub fn fill_rect(&mut self, rect: Rect, color: Color) {
self.fill(rect, color);
}
pub fn fill_rounded_rect(&mut self, rect: Rect, radius: f64, color: Color) {
self.fill(RoundedRect::from_rect(rect, radius), color);
}
pub fn stroke(&mut self, shape: impl Shape, stroke: Stroke) {
self.draw(shape, Paint::Stroke(stroke), Fill::NonZero);
}
pub fn line(&mut self, from: Point, to: Point, stroke: Stroke) {
self.stroke(kurbo::Line::new(from, to), stroke);
}
pub fn draw(&mut self, shape: impl Shape, paint: Paint, rule: Fill) {
let path = shape.into_path(0.1);
if path.elements().is_empty() {
return;
}
let operator = paint_operator(&paint, rule);
self.out.push_str("q\n");
self.set_paint(paint);
self.write_path(&path);
self.out.push_str(operator);
self.out.push_str("\nQ\n");
}
pub fn text(&mut self, text: &str, font: &EmbeddedFont, size: f64, at: Point, color: Color) {
let codes = match font.encode_checked(text) {
Ok(codes) => codes,
Err(missing) => return self.fail(pdfrum_edit::Error::from(missing).into()),
};
if codes.is_empty() {
return;
}
let name = self.realize(pdf_names::FONT, Object::Ref(font.object()));
self.out.push_str("q\n");
self.set_paint(Paint::Fill(color));
self.out.push_str("BT\n/");
self.push_name(&name);
self.out.push(' ');
write_f64(&mut self.out, size);
self.out.push_str(" Tf 1 0 0 1 ");
write_point(&mut self.out, at);
self.out.push_str(" Tm ");
write_hex_string(&mut self.out, &codes);
self.out.push_str(" Tj\nET\nQ\n");
}
#[must_use]
pub fn text_width(&self, text: &str, font: &EmbeddedFont, size: f64) -> f64 {
let Ok(codes) = font.encode_checked(text) else {
return 0.0;
};
self.edit.string_width(font.object(), &codes) * size / 1000.0
}
pub fn image(&mut self, image: &EmbeddedImage, rect: Rect) {
if rect.width() == 0.0 || rect.height() == 0.0 {
return;
}
let name = self.realize(pdf_names::XOBJECT, Object::Ref(image.object()));
self.out.push_str("q\n");
write_matrix(
&mut self.out,
Affine::new([rect.width(), 0.0, 0.0, rect.height(), rect.x0, rect.y0]),
);
self.out.push_str(" cm /");
self.push_name(&name);
self.out.push_str(" Do\nQ\n");
}
pub fn opacity(&mut self, alpha: f64) {
let alpha = alpha.clamp(0.0, 1.0);
let state = Dict::from_pairs([
(Name::from("ca"), Object::Real(as_f32(alpha))),
(Name::from("CA"), Object::Real(as_f32(alpha))),
]);
let name = self.realize(pdf_names::EXT_G_STATE, Object::Dict(state));
self.out.push('/');
self.push_name(&name);
self.out.push_str(" gs\n");
}
#[cfg(feature = "svg-import")]
pub(crate) fn shade(
&mut self,
shape: &BezPath,
rule: Fill,
shading: &Dict,
transform: Affine,
opacity: f64,
) {
let name = self.realize(pdf_names::SHADING, Object::Dict(shading.clone()));
self.out.push_str("q\n");
self.write_path(shape);
self.out.push_str(match rule {
Fill::NonZero => " W n\n",
Fill::EvenOdd => " W* n\n",
});
if opacity < 1.0 {
self.opacity(opacity);
}
write_matrix(&mut self.out, transform);
self.out.push_str(" cm /");
self.push_name(&name);
self.out.push_str(" sh\nQ\n");
}
#[cfg(feature = "svg-import")]
pub(crate) fn embed_svg_image(&mut self, bytes: &[u8]) -> Option<EmbeddedImage> {
if bytes.starts_with(&[0xFF, 0xD8]) {
return self.edit.embed_jpeg(bytes).ok();
}
let decoded = crate::svg_ingest::decode_png(bytes)?;
self.edit
.embed_image(
&decoded.pixels,
decoded.width,
decoded.height,
decoded.format,
)
.ok()
}
#[cfg(feature = "svg-text")]
pub(crate) fn session(&self) -> &DocEdit<'_> {
self.edit
}
fn fail(&mut self, error: Error) {
if self.failed.is_none() {
self.failed = Some(error);
}
}
fn set_paint(&mut self, paint: Paint) {
let (fill, stroke) = match paint {
Paint::Fill(color) => (Some(color), None),
Paint::Stroke(stroke) => (None, Some(stroke)),
Paint::FillStroke(color, stroke) => (Some(color), Some(stroke)),
};
let alpha = fill
.map(alpha_of)
.into_iter()
.chain(stroke.as_ref().map(|stroke| alpha_of(stroke.color)))
.fold(1.0_f64, f64::min);
if alpha < 1.0 {
self.opacity(alpha);
}
if let Some(color) = fill {
self.write_rgb(color);
self.out.push_str(" rg\n");
}
if let Some(stroke) = stroke {
self.write_rgb(stroke.color);
self.out.push_str(" RG\n");
write_f64(&mut self.out, stroke.width.max(0.0));
self.out.push_str(" w\n");
self.write_pen(&stroke);
}
}
fn write_pen(&mut self, stroke: &Stroke) {
if stroke.cap != LineCap::Butt {
let _ = writeln!(self.out, "{} J", stroke.cap.operand());
}
if stroke.join != LineJoin::Miter {
let _ = writeln!(self.out, "{} j", stroke.join.operand());
}
if stroke.miter_limit != MiterLimit::default() {
write_f64(&mut self.out, stroke.miter_limit.get());
self.out.push_str(" M\n");
}
if let Some(dash) = &stroke.dash {
self.out.push('[');
for (i, length) in dash.lengths().iter().enumerate() {
if i > 0 {
self.out.push(' ');
}
write_f64(&mut self.out, *length);
}
self.out.push_str("] ");
write_f64(&mut self.out, dash.phase());
self.out.push_str(" d\n");
}
}
fn write_rgb(&mut self, color: Color) {
let [r, g, b, _] = color.components;
for (i, component) in [r, g, b].into_iter().enumerate() {
if i > 0 {
self.out.push(' ');
}
write_float(&mut self.out, component.clamp(0.0, 1.0));
}
}
fn write_path(&mut self, path: &BezPath) {
if let Some(rect) = axis_aligned_rect(path) {
pdfrum_edit::write_rect(&mut self.out, rect);
self.out.push_str(" re");
return;
}
let mut at = Point::ZERO;
let mut start = Point::ZERO;
for (index, element) in path.elements().iter().enumerate() {
if index > 0 {
self.out.push(' ');
}
match *element {
PathEl::MoveTo(p) => {
write_point(&mut self.out, p);
self.out.push_str(" m");
at = p;
start = p;
}
PathEl::LineTo(p) => {
write_point(&mut self.out, p);
self.out.push_str(" l");
at = p;
}
PathEl::QuadTo(c, p) => {
let (c1, c2) = quad_to_cubic(at, c, p);
self.write_cubic(c1, c2, p);
at = p;
}
PathEl::CurveTo(c1, c2, p) => {
self.write_cubic(c1, c2, p);
at = p;
}
PathEl::ClosePath => {
self.out.push('h');
at = start;
}
}
}
}
fn write_cubic(&mut self, c1: Point, c2: Point, end: Point) {
write_point(&mut self.out, c1);
self.out.push(' ');
write_point(&mut self.out, c2);
self.out.push(' ');
write_point(&mut self.out, end);
self.out.push_str(" c");
}
fn push_name(&mut self, name: &Name) {
for byte in name.as_bytes() {
if byte.is_ascii_alphanumeric() {
self.out.push(char::from(*byte));
} else {
let _ = write!(self.out, "#{byte:02X}");
}
}
}
fn realize(&mut self, category: &'static Name, value: Object) -> Name {
if let Some((_, name, _)) = self
.added
.iter()
.find(|(held_category, _, held)| *held_category == category && *held == value)
{
return name.clone();
}
let name = self.free_name(category);
self.taken.push((category, name.clone()));
self.added.push((category, name.clone(), value));
name
}
fn free_name(&self, category: &Name) -> Name {
for id in 1u32.. {
let candidate = Name::from(format!("PdfrumC{id}").as_str());
if !self
.taken
.iter()
.any(|(cat, name)| *cat == category && *name == candidate)
{
return candidate;
}
}
Name::from("PdfrumC1")
}
}
#[cfg(feature = "svg-import")]
#[derive(Debug, Clone, PartialEq)]
pub struct SvgForm {
object: ObjRef,
bbox: Rect,
}
#[cfg(feature = "svg-import")]
impl SvgForm {
#[must_use]
pub fn bbox(&self) -> Rect {
self.bbox
}
}
#[cfg(feature = "svg-import")]
impl Canvas<'_, '_> {
pub(crate) fn place_form(&mut self, form: &SvgForm, into: Rect) {
if into.width() == 0.0 || into.height() == 0.0 || form.bbox.is_zero_area() {
return;
}
let name = self.realize(pdf_names::XOBJECT, Object::Ref(form.object));
let scale_x = into.width() / form.bbox.width();
let scale_y = into.height() / form.bbox.height();
let placement = Affine::new([
scale_x,
0.0,
0.0,
scale_y,
into.x0 - form.bbox.x0 * scale_x,
into.y0 - form.bbox.y0 * scale_y,
]);
self.out.push_str("q\n");
self.write_path(&into.into_path(0.1));
self.out.push_str(" W n\n");
write_matrix(&mut self.out, placement);
self.out.push_str(" cm /");
self.push_name(&name);
self.out.push_str(" Do\nQ\n");
}
}
#[cfg(feature = "svg-import")]
impl DocEdit<'_> {
pub(crate) fn compile_form(
&mut self,
bbox: Rect,
body: impl FnOnce(&mut Canvas<'_, '_>),
) -> Result<SvgForm> {
let mut canvas = Canvas {
out: String::new(),
edit: self,
added: Vec::new(),
taken: Vec::new(),
size: bbox.size(),
surface: Surface::Form,
failed: None,
};
body(&mut canvas);
if let Some(error) = canvas.failed {
return Err(error);
}
let Canvas { out, added, .. } = canvas;
let resources = merge_resources(&Dict::new(), &added);
let bytes = out.into_bytes();
let dict = Dict::from_pairs([
(
pdf_names::TYPE.clone(),
Object::Name(pdf_names::XOBJECT.clone()),
),
(pdf_names::SUBTYPE.clone(), Object::Name(Name::from("Form"))),
(Name::from("FormType"), Object::Int(1)),
(Name::from("BBox"), Object::Array(rect_array(bbox))),
(pdf_names::RESOURCES.clone(), Object::Dict(resources)),
(
pdf_names::LENGTH.clone(),
Object::Int(i64::try_from(bytes.len()).unwrap_or(0)),
),
]);
let object = self
.inner
.add(Object::Stream(Box::new(Stream::new(dict, bytes.into()))));
Ok(SvgForm { object, bbox })
}
}
#[cfg(feature = "svg-import")]
fn rect_array(rect: Rect) -> Array {
Array::of([rect.x0, rect.y0, rect.x1, rect.y1].map(|value| Object::Real(as_f32(value))))
}
#[expect(
clippy::float_cmp,
reason = "exact recognition of a caller-built rectangle; a tolerance here would move an edge"
)]
fn axis_aligned_rect(path: &BezPath) -> Option<Rect> {
let corners: [Point; 4] = match path.elements() {
[
PathEl::MoveTo(first),
PathEl::LineTo(second),
PathEl::LineTo(third),
PathEl::LineTo(fourth),
PathEl::ClosePath,
] => [*first, *second, *third, *fourth],
[
PathEl::MoveTo(first),
PathEl::LineTo(second),
PathEl::LineTo(third),
PathEl::LineTo(fourth),
PathEl::LineTo(back),
PathEl::ClosePath,
] if back == first => [*first, *second, *third, *fourth],
_ => return None,
};
for index in 0..4 {
let from = *corners.get(index)?;
let to = *corners.get((index + 1) % 4)?;
if from.x != to.x && from.y != to.y {
return None;
}
}
let (origin, opposite) = (*corners.first()?, *corners.get(2)?);
if origin.x == opposite.x || origin.y == opposite.y {
return None;
}
Some(Rect::new(origin.x, origin.y, opposite.x, opposite.y))
}
fn alpha_of(color: Color) -> f64 {
f64::from(color.components[3]).clamp(0.0, 1.0)
}
#[expect(
clippy::cast_possible_truncation,
reason = "PDF numbers are f32; the geometry vocabulary is f64"
)]
fn as_f32(value: f64) -> f32 {
value as f32
}
fn write_f64(out: &mut String, value: f64) {
write_float(out, as_f32(value));
}
fn quad_to_cubic(previous: Point, control: Point, end: Point) -> (Point, Point) {
let third = 2.0 / 3.0;
(
previous + (control - previous) * third,
end + (control - end) * third,
)
}
fn paint_operator(paint: &Paint, rule: Fill) -> &'static str {
match (paint, rule) {
(Paint::Fill(_), Fill::NonZero) => " f",
(Paint::Fill(_), Fill::EvenOdd) => " f*",
(Paint::Stroke(_), _) => " S",
(Paint::FillStroke(_, _), Fill::NonZero) => " B",
(Paint::FillStroke(_, _), Fill::EvenOdd) => " B*",
}
}
fn write_hex_string(out: &mut String, codes: &[u8]) {
out.push('<');
for byte in codes {
let _ = write!(out, "{byte:02X}");
}
out.push('>');
}
impl DocEdit<'_> {
pub fn draw_page(
&mut self,
index: impl Into<PageIndex>,
body: impl FnOnce(&mut Canvas<'_, '_>),
) -> Result<()> {
let index = index.into();
let Some((reference, dict, resources)) = self.page_state(index)? else {
return Err(pdfrum_edit::Error::InlinePage(index).into());
};
let (to_page, size) = self.canvas_space(reference, &dict);
let taken = existing_names(&resources, &self.inner);
let mut canvas = Canvas {
out: String::new(),
edit: self,
added: Vec::new(),
taken,
size,
surface: Surface::Page(index),
failed: None,
};
body(&mut canvas);
if let Some(error) = canvas.failed {
return Err(error);
}
let Canvas { out, added, .. } = canvas;
if out.is_empty() {
return Ok(());
}
let mut bytes = String::with_capacity(out.len() + 64);
bytes.push_str("q\n");
write_matrix(&mut bytes, to_page);
bytes.push_str(" cm\n");
bytes.push_str(&out);
bytes.push_str("Q\n");
self.append_stream(reference, &dict, &resources, bytes.as_bytes(), &added);
Ok(())
}
pub fn draw_pages(&mut self, mut body: impl FnMut(&mut Canvas<'_, '_>)) -> Result<()> {
for index in 0..self.doc.page_count() {
let index = PageIndex::from(index);
if self.page_state(index)?.is_none() {
continue;
}
self.draw_page(index, &mut body)?;
}
Ok(())
}
fn canvas_space(&self, reference: ObjRef, dict: &Dict) -> (Affine, kurbo::Size) {
let page = pdfrum_parser::PageDict {
dict: dict.clone(),
reference: Some(reference),
};
let mut diags = Diagnostics::default();
let (_, crop) = pdfrum_page::derive_boxes(
&page.dict,
|key| page.inherited(key, &self.inner),
&self.inner,
&mut diags,
);
self.doc.note(&diags);
let rotate_key = Name::from("Rotate");
let rotate = pdfrum_page::Rotation::from_degrees(
page.dict
.raw(&rotate_key)
.cloned()
.or_else(|| page.inherited(&rotate_key, &self.inner))
.and_then(|value| value.resolve(&self.inner).ok()?.get().as_int())
.unwrap_or(0),
);
let size = if rotate.quarters().is_multiple_of(2) {
kurbo::Size::new(crop.width(), crop.height())
} else {
kurbo::Size::new(crop.height(), crop.width())
};
(rotate.display_matrix(crop).inverse(), size)
}
pub(crate) fn string_width(&self, font: ObjRef, codes: &[u8]) -> f64 {
let mut diags = Diagnostics::default();
let loaded = self
.inner
.fetch(font)
.ok()
.as_deref()
.and_then(Object::as_dict)
.and_then(|dict| {
pdfrum_font::load(
dict,
&self.inner,
&pdfrum_font::FontCache::new(),
&self.doc.limits,
&mut diags,
)
});
match loaded {
Some(metrics) => f64::from(metrics.string_width(codes)),
None => 500.0 * f64::from(u32::try_from(codes.len()).unwrap_or(u32::MAX)),
}
}
fn append_stream(
&mut self,
reference: ObjRef,
dict: &Dict,
resources: &Dict,
bytes: &[u8],
added: &[(&'static Name, Name, Object)],
) {
let stream = Stream::new(
Dict::from_pairs([(
pdf_names::LENGTH.clone(),
Object::Int(i64::try_from(bytes.len()).unwrap_or(0)),
)]),
ByteSpan::from(bytes.to_vec()),
);
let fresh = self.inner.add(Object::Stream(Box::new(stream)));
let shape = ContentsShape::read(dict, &self.inner);
let (_, next) = shape.with_added(fresh);
let shared = pdfrum_edit::shared_objects(&self.inner);
let mut dict = dict.clone();
let elements = next.elements();
let array = Object::Array(Array::of(elements.iter().map(|e| Object::Ref(*e))));
let reusable = matches!(
dict.raw(pdf_names::CONTENTS),
Some(Object::Ref(r)) if !shared.contains(&r.num) && !elements.contains(r)
);
let contents = match dict.raw(pdf_names::CONTENTS) {
Some(Object::Ref(existing)) if reusable => {
let existing = *existing;
self.inner.replace(existing, array);
Object::Ref(existing)
}
_ => Object::Ref(self.inner.add(array)),
};
dict = with_key(&dict, pdf_names::CONTENTS, contents);
let merged = merge_resources(resources, added);
match dict.raw(pdf_names::RESOURCES) {
Some(Object::Ref(existing)) if !shared.contains(&existing.num) => {
let existing = *existing;
self.inner.replace(existing, Object::Dict(merged));
}
_ => dict = with_key(&dict, pdf_names::RESOURCES, Object::Dict(merged)),
}
self.inner.replace(reference, Object::Dict(dict));
}
}
fn existing_names(resources: &Dict, r: &impl Resolve) -> Vec<(&'static Name, Name)> {
let mut taken = Vec::new();
for category in [pdf_names::FONT, pdf_names::XOBJECT, pdf_names::EXT_G_STATE] {
let Some(sub) = resources.dict(category, r) else {
continue;
};
for (name, _) in sub.iter() {
taken.push((category, name.clone()));
}
}
taken
}
fn merge_resources(resources: &Dict, added: &[(&'static Name, Name, Object)]) -> Dict {
let mut out = Dict::new();
for (key, value) in resources.iter() {
let extra: Vec<_> = added
.iter()
.filter(|(category, _, _)| *category == key)
.collect();
if extra.is_empty() {
out.push(key.clone(), value.clone());
continue;
}
let mut sub = match value {
Object::Dict(dict) => dict.clone(),
_ => Dict::new(),
};
for (_, name, held) in extra {
sub.push(name.clone(), held.clone());
}
out.push(key.clone(), Object::Dict(sub));
}
for (category, _, _) in added {
if out.contains_key(category) {
continue;
}
let mut sub = Dict::new();
for (_, name, held) in added.iter().filter(|(cat, _, _)| cat == category) {
sub.push(name.clone(), held.clone());
}
if !sub.is_empty() {
out.push((*category).clone(), Object::Dict(sub));
}
}
out
}
fn with_key(dict: &Dict, key: &Name, value: Object) -> Dict {
let mut out = Dict::new();
let mut written = false;
for (existing, held) in dict.iter() {
if existing == key {
if !written {
out.push(existing.clone(), value.clone());
written = true;
}
} else {
out.push(existing.clone(), held.clone());
}
}
if !written {
out.push(key.clone(), value);
}
out
}