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}