Skip to main content

pdfsink_rs/
types.rs

1use serde::{Deserialize, Serialize};
2use serde_json::Value;
3use std::collections::BTreeMap;
4use std::path::PathBuf;
5
6pub type JsonMap = BTreeMap<String, Value>;
7
8#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq)]
9pub struct BBox {
10    pub x0: f64,
11    pub top: f64,
12    pub x1: f64,
13    pub bottom: f64,
14}
15
16impl BBox {
17    pub const fn new(x0: f64, top: f64, x1: f64, bottom: f64) -> Self {
18        Self { x0, top, x1, bottom }
19    }
20
21    pub fn width(self) -> f64 {
22        self.x1 - self.x0
23    }
24
25    pub fn height(self) -> f64 {
26        self.bottom - self.top
27    }
28
29    pub fn area(self) -> f64 {
30        self.width() * self.height()
31    }
32
33    pub fn is_valid(self) -> bool {
34        self.x0 <= self.x1 && self.top <= self.bottom && self.is_finite()
35    }
36
37    pub fn is_finite(self) -> bool {
38        self.x0.is_finite() && self.top.is_finite() && self.x1.is_finite() && self.bottom.is_finite()
39    }
40
41    pub fn is_empty(self) -> bool {
42        self.width() == 0.0 || self.height() == 0.0
43    }
44
45    pub fn normalized(self) -> Self {
46        Self::new(
47            self.x0.min(self.x1),
48            self.top.min(self.bottom),
49            self.x0.max(self.x1),
50            self.top.max(self.bottom),
51        )
52    }
53
54    pub fn overlaps(self, other: Self) -> bool {
55        self.overlap(other).is_some()
56    }
57
58    pub fn intersects(self, other: Self) -> bool {
59        self.overlaps(other)
60    }
61
62    pub fn contains_point(self, point: Point) -> bool {
63        let bbox = self.normalized();
64        point.x >= bbox.x0 && point.x <= bbox.x1 && point.y >= bbox.top && point.y <= bbox.bottom
65    }
66
67    pub fn contains_bbox(self, other: Self) -> bool {
68        let bbox = self.normalized();
69        let other = other.normalized();
70        bbox.x0 <= other.x0
71            && bbox.top <= other.top
72            && bbox.x1 >= other.x1
73            && bbox.bottom >= other.bottom
74    }
75
76    pub fn overlap(self, other: Self) -> Option<Self> {
77        let bbox = self.normalized();
78        let other = other.normalized();
79        if !bbox.is_finite() || !other.is_finite() {
80            return None;
81        }
82
83        let x0 = bbox.x0.max(other.x0);
84        let top = bbox.top.max(other.top);
85        let x1 = bbox.x1.min(other.x1);
86        let bottom = bbox.bottom.min(other.bottom);
87        if x1 >= x0 && bottom >= top && ((x1 - x0) + (bottom - top) > 0.0) {
88            Some(Self::new(x0, top, x1, bottom))
89        } else {
90            None
91        }
92    }
93
94    pub fn intersection(self, other: Self) -> Option<Self> {
95        self.overlap(other)
96    }
97
98    pub fn intersection_area(self, other: Self) -> f64 {
99        self.overlap(other)
100            .map(|bbox| bbox.width().max(0.0) * bbox.height().max(0.0))
101            .unwrap_or(0.0)
102    }
103
104    pub fn overlap_ratio(self, other: Self) -> f64 {
105        let area = self.normalized().area();
106        if area <= 0.0 {
107            0.0
108        } else {
109            self.intersection_area(other) / area
110        }
111    }
112
113    pub fn intersection_over_union(self, other: Self) -> f64 {
114        let a = self.normalized().area().max(0.0);
115        let b = other.normalized().area().max(0.0);
116        let intersection = self.intersection_area(other);
117        let union = a + b - intersection;
118        if union <= 0.0 {
119            0.0
120        } else {
121            intersection / union
122        }
123    }
124
125    pub fn union(self, other: Self) -> Self {
126        let bbox = self.normalized();
127        let other = other.normalized();
128        Self::new(
129            bbox.x0.min(other.x0),
130            bbox.top.min(other.top),
131            bbox.x1.max(other.x1),
132            bbox.bottom.max(other.bottom),
133        )
134    }
135
136    pub fn clamp(self, bounds: Self) -> Option<Self> {
137        self.overlap(bounds)
138    }
139
140    pub fn expand(self, dx: f64, dy: f64) -> Self {
141        Self::new(self.x0 - dx, self.top - dy, self.x1 + dx, self.bottom + dy)
142    }
143
144    pub fn pad(self, amount: f64) -> Self {
145        self.expand(amount, amount)
146    }
147
148    pub fn round(self, precision: usize) -> Self {
149        let factor = 10f64.powi(precision as i32);
150        Self::new(
151            (self.x0 * factor).round() / factor,
152            (self.top * factor).round() / factor,
153            (self.x1 * factor).round() / factor,
154            (self.bottom * factor).round() / factor,
155        )
156    }
157
158    pub fn translate(self, dx: f64, dy: f64) -> Self {
159        Self::new(self.x0 + dx, self.top + dy, self.x1 + dx, self.bottom + dy)
160    }
161
162    pub fn as_tuple(self) -> (f64, f64, f64, f64) {
163        (self.x0, self.top, self.x1, self.bottom)
164    }
165
166    pub fn center(self) -> Point {
167        Point::new((self.x0 + self.x1) / 2.0, (self.top + self.bottom) / 2.0)
168    }
169}
170
171impl Default for BBox {
172    fn default() -> Self {
173        Self::new(0.0, 0.0, 0.0, 0.0)
174    }
175}
176
177#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq)]
178pub struct Point {
179    pub x: f64,
180    pub y: f64,
181}
182
183impl Point {
184    pub const fn new(x: f64, y: f64) -> Self {
185        Self { x, y }
186    }
187}
188
189#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
190#[serde(rename_all = "snake_case")]
191pub enum Direction {
192    Ttb,
193    Btt,
194    Ltr,
195    Rtl,
196}
197
198impl Direction {
199    pub fn as_str(self) -> &'static str {
200        match self {
201            Self::Ttb => "ttb",
202            Self::Btt => "btt",
203            Self::Ltr => "ltr",
204            Self::Rtl => "rtl",
205        }
206    }
207
208    pub fn is_horizontal(self) -> bool {
209        matches!(self, Self::Ltr | Self::Rtl)
210    }
211
212    pub fn is_vertical(self) -> bool {
213        matches!(self, Self::Ttb | Self::Btt)
214    }
215}
216
217impl std::str::FromStr for Direction {
218    type Err = crate::Error;
219
220    fn from_str(s: &str) -> crate::Result<Self> {
221        match s {
222            "ttb" => Ok(Self::Ttb),
223            "btt" => Ok(Self::Btt),
224            "ltr" => Ok(Self::Ltr),
225            "rtl" => Ok(Self::Rtl),
226            other => Err(crate::Error::Message(format!("unknown direction: {other}"))),
227        }
228    }
229}
230
231#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
232#[serde(rename_all = "snake_case")]
233pub enum Orientation {
234    Horizontal,
235    Vertical,
236}
237
238impl Orientation {
239    pub fn as_char(self) -> &'static str {
240        match self {
241            Self::Horizontal => "h",
242            Self::Vertical => "v",
243        }
244    }
245}
246
247#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
248pub struct Char {
249    pub object_type: String,
250    pub page_number: usize,
251    pub text: String,
252    pub x0: f64,
253    pub top: f64,
254    pub x1: f64,
255    pub bottom: f64,
256    pub y0: f64,
257    pub y1: f64,
258    pub doctop: f64,
259    pub width: f64,
260    pub height: f64,
261    pub size: f64,
262    pub adv: f64,
263    pub upright: bool,
264    pub fontname: String,
265    pub matrix: [f64; 6],
266    #[serde(skip_serializing_if = "Option::is_none")]
267    pub mcid: Option<i64>,
268    #[serde(skip_serializing_if = "Option::is_none")]
269    pub tag: Option<String>,
270    #[serde(skip_serializing_if = "Option::is_none")]
271    pub ncs: Option<String>,
272    #[serde(skip_serializing_if = "Option::is_none")]
273    pub stroking_color: Option<String>,
274    #[serde(skip_serializing_if = "Option::is_none")]
275    pub non_stroking_color: Option<String>,
276}
277
278#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
279pub struct Word {
280    pub text: String,
281    pub x0: f64,
282    pub top: f64,
283    pub x1: f64,
284    pub bottom: f64,
285    pub doctop: f64,
286    pub width: f64,
287    pub height: f64,
288    pub upright: bool,
289    pub direction: Direction,
290    #[serde(skip_serializing_if = "Option::is_none")]
291    pub chars: Option<Vec<Char>>,
292}
293
294#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
295pub struct Line {
296    pub object_type: String,
297    pub page_number: usize,
298    pub x0: f64,
299    pub top: f64,
300    pub x1: f64,
301    pub bottom: f64,
302    pub y0: f64,
303    pub y1: f64,
304    pub doctop: f64,
305    pub width: f64,
306    pub height: f64,
307    pub pts: Vec<Point>,
308    pub stroke: bool,
309    pub fill: bool,
310    pub linewidth: f64,
311}
312
313#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
314pub enum PathCommand {
315    MoveTo(Point),
316    LineTo(Point),
317    CurveTo { c1: Point, c2: Point, p: Point },
318    Rect { x: f64, y: f64, width: f64, height: f64 },
319    Close,
320}
321
322#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
323pub struct RectObject {
324    pub object_type: String,
325    pub page_number: usize,
326    pub x0: f64,
327    pub top: f64,
328    pub x1: f64,
329    pub bottom: f64,
330    pub y0: f64,
331    pub y1: f64,
332    pub doctop: f64,
333    pub width: f64,
334    pub height: f64,
335    pub pts: Vec<Point>,
336    pub path: Vec<PathCommand>,
337    pub stroke: bool,
338    pub fill: bool,
339    pub linewidth: f64,
340}
341
342#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
343pub struct Curve {
344    pub object_type: String,
345    pub page_number: usize,
346    pub x0: f64,
347    pub top: f64,
348    pub x1: f64,
349    pub bottom: f64,
350    pub y0: f64,
351    pub y1: f64,
352    pub doctop: f64,
353    pub width: f64,
354    pub height: f64,
355    pub pts: Vec<Point>,
356    pub path: Vec<PathCommand>,
357    pub stroke: bool,
358    pub fill: bool,
359    pub linewidth: f64,
360}
361
362#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
363pub struct ImageObject {
364    pub object_type: String,
365    pub page_number: usize,
366    pub x0: f64,
367    pub top: f64,
368    pub x1: f64,
369    pub bottom: f64,
370    pub y0: f64,
371    pub y1: f64,
372    pub doctop: f64,
373    pub width: f64,
374    pub height: f64,
375    pub name: String,
376    pub srcsize: (u32, u32),
377    #[serde(skip_serializing_if = "Option::is_none")]
378    pub bits: Option<i64>,
379    #[serde(skip_serializing_if = "Option::is_none")]
380    pub colorspace: Option<String>,
381    #[serde(skip_serializing_if = "Option::is_none")]
382    pub imagemask: Option<bool>,
383    #[serde(skip_serializing_if = "Option::is_none")]
384    pub mcid: Option<i64>,
385    #[serde(skip_serializing_if = "Option::is_none")]
386    pub tag: Option<String>,
387}
388
389#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
390pub struct Annotation {
391    pub object_type: String,
392    pub page_number: usize,
393    pub x0: f64,
394    pub top: f64,
395    pub x1: f64,
396    pub bottom: f64,
397    pub y0: f64,
398    pub y1: f64,
399    pub doctop: f64,
400    pub width: f64,
401    pub height: f64,
402    pub subtype: String,
403    #[serde(skip_serializing_if = "Option::is_none")]
404    pub uri: Option<String>,
405    #[serde(skip_serializing_if = "Option::is_none")]
406    pub title: Option<String>,
407    #[serde(skip_serializing_if = "Option::is_none")]
408    pub contents: Option<String>,
409}
410
411#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
412pub struct Hyperlink {
413    pub object_type: String,
414    pub page_number: usize,
415    pub x0: f64,
416    pub top: f64,
417    pub x1: f64,
418    pub bottom: f64,
419    pub y0: f64,
420    pub y1: f64,
421    pub doctop: f64,
422    pub width: f64,
423    pub height: f64,
424    pub uri: String,
425}
426
427#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
428pub struct Edge {
429    pub x0: f64,
430    pub top: f64,
431    pub x1: f64,
432    pub bottom: f64,
433    pub width: f64,
434    pub height: f64,
435    pub orientation: Orientation,
436    pub object_type: String,
437}
438
439#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Default)]
440pub struct ObjectCounts {
441    pub chars: usize,
442    pub lines: usize,
443    pub rects: usize,
444    pub curves: usize,
445    pub images: usize,
446    pub annots: usize,
447    pub hyperlinks: usize,
448}
449
450impl ObjectCounts {
451    pub fn total(&self) -> usize {
452        self.chars + self.lines + self.rects + self.curves + self.images + self.annots + self.hyperlinks
453    }
454
455    pub fn is_empty(&self) -> bool {
456        self.total() == 0
457    }
458}
459
460impl std::ops::Add for ObjectCounts {
461    type Output = Self;
462
463    fn add(self, rhs: Self) -> Self::Output {
464        Self {
465            chars: self.chars + rhs.chars,
466            lines: self.lines + rhs.lines,
467            rects: self.rects + rhs.rects,
468            curves: self.curves + rhs.curves,
469            images: self.images + rhs.images,
470            annots: self.annots + rhs.annots,
471            hyperlinks: self.hyperlinks + rhs.hyperlinks,
472        }
473    }
474}
475
476impl std::ops::AddAssign for ObjectCounts {
477    fn add_assign(&mut self, rhs: Self) {
478        self.chars += rhs.chars;
479        self.lines += rhs.lines;
480        self.rects += rhs.rects;
481        self.curves += rhs.curves;
482        self.images += rhs.images;
483        self.annots += rhs.annots;
484        self.hyperlinks += rhs.hyperlinks;
485    }
486}
487
488#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
489pub struct SearchMatch {
490    pub text: String,
491    pub x0: f64,
492    pub top: f64,
493    pub x1: f64,
494    pub bottom: f64,
495    #[serde(skip_serializing_if = "Option::is_none")]
496    pub groups: Option<Vec<Option<String>>>,
497    #[serde(skip_serializing_if = "Option::is_none")]
498    pub chars: Option<Vec<Char>>,
499}
500
501#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
502pub struct TextLine {
503    pub text: String,
504    pub x0: f64,
505    pub top: f64,
506    pub x1: f64,
507    pub bottom: f64,
508    #[serde(skip_serializing_if = "Option::is_none")]
509    pub chars: Option<Vec<Char>>,
510}
511
512#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
513pub struct LayoutObject {
514    pub object_type: String,
515    pub page_number: usize,
516    pub x0: f64,
517    pub top: f64,
518    pub x1: f64,
519    pub bottom: f64,
520    pub width: f64,
521    pub height: f64,
522    #[serde(skip_serializing_if = "Option::is_none")]
523    pub text: Option<String>,
524    #[serde(skip_serializing_if = "Option::is_none")]
525    pub direction: Option<Direction>,
526    #[serde(skip_serializing_if = "Option::is_none")]
527    pub upright: Option<bool>,
528    #[serde(default, skip_serializing_if = "Vec::is_empty")]
529    pub children: Vec<LayoutObject>,
530}
531
532impl LayoutObject {
533    pub fn bbox(&self) -> BBox {
534        BBox::new(self.x0, self.top, self.x1, self.bottom)
535    }
536}
537
538#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
539pub struct PageLayout {
540    pub page_number: usize,
541    pub bbox: BBox,
542    pub objects: Vec<LayoutObject>,
543}
544
545#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Default)]
546pub struct StructureElement {
547    pub kind: String,
548    #[serde(skip_serializing_if = "Option::is_none")]
549    pub title: Option<String>,
550    #[serde(skip_serializing_if = "Option::is_none")]
551    pub alt: Option<String>,
552    #[serde(skip_serializing_if = "Option::is_none")]
553    pub page_number: Option<usize>,
554    #[serde(skip_serializing_if = "Option::is_none")]
555    pub mcid: Option<i64>,
556    #[serde(default, skip_serializing_if = "Vec::is_empty")]
557    pub children: Vec<StructureElement>,
558}
559
560#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
561pub struct Page {
562    pub page_number: usize,
563    pub rotation: i32,
564    pub width: f64,
565    pub height: f64,
566    pub bbox: BBox,
567    pub mediabox: BBox,
568    pub cropbox: BBox,
569    #[serde(skip_serializing_if = "Option::is_none")]
570    pub trimbox: Option<BBox>,
571    #[serde(skip_serializing_if = "Option::is_none")]
572    pub bleedbox: Option<BBox>,
573    #[serde(skip_serializing_if = "Option::is_none")]
574    pub artbox: Option<BBox>,
575    pub doctop_offset: f64,
576    pub is_original: bool,
577    pub chars: Vec<Char>,
578    pub lines: Vec<Line>,
579    pub rects: Vec<RectObject>,
580    pub curves: Vec<Curve>,
581    pub images: Vec<ImageObject>,
582    pub annots: Vec<Annotation>,
583    pub hyperlinks: Vec<Hyperlink>,
584    #[serde(skip_serializing_if = "Option::is_none")]
585    pub structure_tree: Option<StructureElement>,
586}
587
588#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
589pub struct PdfDocument {
590    pub path: PathBuf,
591    pub pages: Vec<Page>,
592    #[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
593    pub metadata: JsonMap,
594    #[serde(skip_serializing_if = "Option::is_none")]
595    pub structure_tree: Option<StructureElement>,
596}
597
598pub enum PageObjectRef<'a> {
599    Char(&'a Char),
600    Line(&'a Line),
601    Rect(&'a RectObject),
602    Curve(&'a Curve),
603    Image(&'a ImageObject),
604    Annot(&'a Annotation),
605    Hyperlink(&'a Hyperlink),
606}
607
608impl<'a> PageObjectRef<'a> {
609    pub fn object_type(&self) -> &'static str {
610        match self {
611            Self::Char(_) => "char",
612            Self::Line(_) => "line",
613            Self::Rect(_) => "rect",
614            Self::Curve(_) => "curve",
615            Self::Image(_) => "image",
616            Self::Annot(_) => "annot",
617            Self::Hyperlink(_) => "hyperlink",
618        }
619    }
620
621    pub fn page_number(&self) -> usize {
622        match self {
623            Self::Char(item) => item.page_number,
624            Self::Line(item) => item.page_number,
625            Self::Rect(item) => item.page_number,
626            Self::Curve(item) => item.page_number,
627            Self::Image(item) => item.page_number,
628            Self::Annot(item) => item.page_number,
629            Self::Hyperlink(item) => item.page_number,
630        }
631    }
632
633    pub fn bbox(&self) -> BBox {
634        match self {
635            Self::Char(item) => item.bbox(),
636            Self::Line(item) => item.bbox(),
637            Self::Rect(item) => item.bbox(),
638            Self::Curve(item) => item.bbox(),
639            Self::Image(item) => item.bbox(),
640            Self::Annot(item) => item.bbox(),
641            Self::Hyperlink(item) => item.bbox(),
642        }
643    }
644}
645
646pub trait Bounded: Clone {
647    fn bbox(&self) -> BBox;
648    fn with_bbox(&self, bbox: BBox, page_height: f64) -> Self;
649}
650
651macro_rules! impl_bounded {
652    ($ty:ty) => {
653        impl Bounded for $ty {
654            fn bbox(&self) -> BBox {
655                BBox::new(self.x0, self.top, self.x1, self.bottom)
656            }
657
658            fn with_bbox(&self, bbox: BBox, page_height: f64) -> Self {
659                let mut copy = self.clone();
660                copy.x0 = bbox.x0;
661                copy.top = bbox.top;
662                copy.x1 = bbox.x1;
663                copy.bottom = bbox.bottom;
664                copy.width = bbox.width();
665                copy.height = bbox.height();
666                copy.y0 = page_height - bbox.bottom;
667                copy.y1 = page_height - bbox.top;
668                copy.doctop = (self.doctop - self.top) + bbox.top;
669                copy
670            }
671        }
672    };
673}
674
675impl_bounded!(Char);
676impl_bounded!(Line);
677impl_bounded!(RectObject);
678impl_bounded!(Curve);
679impl_bounded!(ImageObject);
680impl_bounded!(Annotation);
681impl_bounded!(Hyperlink);
682
683impl Bounded for Word {
684    fn bbox(&self) -> BBox {
685        BBox::new(self.x0, self.top, self.x1, self.bottom)
686    }
687
688    fn with_bbox(&self, bbox: BBox, _page_height: f64) -> Self {
689        let mut copy = self.clone();
690        copy.x0 = bbox.x0;
691        copy.top = bbox.top;
692        copy.x1 = bbox.x1;
693        copy.bottom = bbox.bottom;
694        copy.width = bbox.width();
695        copy.height = bbox.height();
696        copy.doctop = (self.doctop - self.top) + bbox.top;
697        copy
698    }
699}
700
701impl Bounded for Edge {
702    fn bbox(&self) -> BBox {
703        BBox::new(self.x0, self.top, self.x1, self.bottom)
704    }
705
706    fn with_bbox(&self, bbox: BBox, _page_height: f64) -> Self {
707        let mut copy = self.clone();
708        copy.x0 = bbox.x0;
709        copy.top = bbox.top;
710        copy.x1 = bbox.x1;
711        copy.bottom = bbox.bottom;
712        copy.width = bbox.width();
713        copy.height = bbox.height();
714        copy
715    }
716}
717
718impl Bounded for LayoutObject {
719    fn bbox(&self) -> BBox {
720        BBox::new(self.x0, self.top, self.x1, self.bottom)
721    }
722
723    fn with_bbox(&self, bbox: BBox, _page_height: f64) -> Self {
724        let mut copy = self.clone();
725        copy.x0 = bbox.x0;
726        copy.top = bbox.top;
727        copy.x1 = bbox.x1;
728        copy.bottom = bbox.bottom;
729        copy.width = bbox.width();
730        copy.height = bbox.height();
731        copy
732    }
733}
734
735#[cfg(test)]
736mod tests {
737    use super::*;
738
739    #[test]
740    fn bbox_normalizes_and_intersects() {
741        let bbox = BBox::new(10.0, 20.0, 0.0, 0.0).normalized();
742        assert_eq!(bbox, BBox::new(0.0, 0.0, 10.0, 20.0));
743        assert!(bbox.contains_point(Point::new(5.0, 5.0)));
744        assert_eq!(bbox.overlap(BBox::new(5.0, 10.0, 15.0, 30.0)), Some(BBox::new(5.0, 10.0, 10.0, 20.0)));
745        assert_eq!(bbox.intersection_area(BBox::new(5.0, 10.0, 15.0, 30.0)), 50.0);
746    }
747
748    #[test]
749    fn object_counts_add_and_total() {
750        let mut counts = ObjectCounts {
751            chars: 1,
752            lines: 2,
753            ..ObjectCounts::default()
754        };
755        counts += ObjectCounts {
756            rects: 3,
757            hyperlinks: 4,
758            ..ObjectCounts::default()
759        };
760        assert_eq!(counts.total(), 10);
761        assert!(!counts.is_empty());
762    }
763}