1use std::fmt;
2
3use serde::ser::SerializeStruct;
4use serde::Serialize;
5
6use crate::outcome::{RenderError, RenderErrorCategory, RenderOutcome};
7use crate::request::{OutputFormat, RenderRequest};
8use crate::scene::SceneDocument;
9
10#[doc(hidden)]
23#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd, Serialize)]
24#[serde(transparent)]
25pub struct CompatibilityRevision(u64);
26
27impl CompatibilityRevision {
28 pub const REVISION_1: Self = Self(1);
30
31 pub const fn get(self) -> u64 {
33 self.0
34 }
35}
36
37impl fmt::Display for CompatibilityRevision {
38 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
39 self.0.fmt(formatter)
40 }
41}
42
43#[doc(hidden)]
46#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq, Serialize)]
47pub struct ReferenceProfileIdentity {
48 compatibility_revision: CompatibilityRevision,
49 excalidraw_release: &'static str,
50 reference_profile_digest: &'static str,
51}
52
53impl ReferenceProfileIdentity {
54 pub(crate) const fn new(
55 compatibility_revision: CompatibilityRevision,
56 excalidraw_release: &'static str,
57 reference_profile_digest: &'static str,
58 ) -> Self {
59 Self {
60 compatibility_revision,
61 excalidraw_release,
62 reference_profile_digest,
63 }
64 }
65
66 pub const fn compatibility_revision(&self) -> CompatibilityRevision {
68 self.compatibility_revision
69 }
70
71 pub const fn excalidraw_release(&self) -> &'static str {
73 self.excalidraw_release
74 }
75
76 pub const fn reference_profile_digest(&self) -> &'static str {
78 self.reference_profile_digest
79 }
80}
81
82#[derive(Clone, Debug)]
87pub struct Renderer {
88 catalog: &'static crate::catalog::RevisionCatalog,
89}
90
91impl Renderer {
92 pub fn new() -> Self {
94 Self {
95 catalog: &crate::catalog::REVISION_1,
96 }
97 }
98
99 #[doc(hidden)]
101 pub const fn profile_identity(&self) -> &'static ReferenceProfileIdentity {
102 self.catalog.profile()
103 }
104
105 #[doc(hidden)]
108 pub const fn frame_profile_identity(&self) -> &'static ReferenceProfileIdentity {
109 self.catalog.frame_profile()
110 }
111
112 pub fn render(
139 &self,
140 scene: &SceneDocument,
141 request: &RenderRequest,
142 ) -> Result<RenderOutcome, RenderError> {
143 crate::render::render(self.catalog, scene, request)
144 }
145
146 pub fn render_frame(
155 &self,
156 frame: crate::FrameRef<'_>,
157 request: &crate::FrameRenderRequest,
158 ) -> Result<RenderOutcome, RenderError> {
159 crate::render::render_frame(self.catalog, frame, request)
160 }
161
162 pub fn render_frames<'scene, 'request>(
174 &self,
175 scene: &'scene SceneDocument,
176 frames: impl IntoIterator<Item = crate::FrameRef<'scene>>,
177 request: &'request crate::FrameRenderRequest,
178 ) -> Result<crate::FrameRenderCursor<'scene, 'request>, crate::FrameBatchError> {
179 crate::FrameRenderCursor::new(self.catalog, scene, frames, request)
180 }
181}
182
183impl Default for Renderer {
184 fn default() -> Self {
185 Self::new()
186 }
187}
188
189#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize)]
191#[non_exhaustive]
192#[serde(rename_all = "snake_case")]
193pub enum ErrorFamily {
194 SceneParse,
196 Request,
198 Render,
200 FrameBatch,
202}
203
204impl ErrorFamily {
205 pub const fn as_str(self) -> &'static str {
207 match self {
208 Self::SceneParse => "scene_parse",
209 Self::Request => "request",
210 Self::Render => "render",
211 Self::FrameBatch => "frame_batch",
212 }
213 }
214}
215
216impl fmt::Display for ErrorFamily {
217 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
218 formatter.write_str(self.as_str())
219 }
220}
221
222#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq, Serialize)]
224#[serde(transparent)]
225pub struct ErrorCode(&'static str);
226
227impl ErrorCode {
228 pub const REQUEST_INVALID_PADDING: Self = Self("request.invalid-padding");
230
231 pub const REQUEST_INVALID_SCALE: Self = Self("request.invalid-scale");
233
234 pub const REQUEST_INVALID_MAX_DIMENSION: Self = Self("request.invalid-max-dimension");
236
237 pub const REQUEST_INCOMPATIBLE_FORMAT_OPTIONS: Self =
239 Self("request.incompatible-format-options");
240
241 pub const REQUEST_EMPTY_ASSET_ID: Self = Self("request.empty-asset-id");
243
244 pub const REQUEST_DUPLICATE_ASSET_ID: Self = Self("request.duplicate-asset-id");
246
247 pub const REQUEST_INVALID_ASSET_MEDIA_TYPE: Self = Self("request.invalid-asset-media-type");
249
250 pub const REQUEST_INVALID_RESOURCE_LIMIT: Self = Self("request.invalid-resource-limit");
252
253 pub const REQUEST_FORMAT_UNAVAILABLE: Self = Self("request.format-unavailable");
256
257 pub const SCENE_UNBOUNDED_GEOMETRY: Self = Self("scene.unbounded-geometry");
259 pub const PDF_RASTER_FALLBACK_FORBIDDEN: Self = Self("pdf.raster-fallback-forbidden");
261 pub const PDF_RASTER_FALLBACK_UNISOLATABLE: Self = Self("pdf.raster-fallback-unisolatable");
263 pub const RENDER_NO_MEANINGFUL_ARTIFACT: Self = Self("render.no-meaningful-artifact");
265 pub const RENDER_FONT_UNAVAILABLE: Self = Self("render.font-unavailable");
267 pub const ENCODE_SVG_FAILED: Self = Self("encode.svg-failed");
269 pub const ENCODE_SVG_FONT_SUBSET_FAILED: Self = Self("encode.svg-font-subset-failed");
271 pub const ENCODE_PNG_FAILED: Self = Self("encode.png-failed");
273 pub const ENCODE_PDF_FAILED: Self = Self("encode.pdf-failed");
275 pub const RENDER_INTERNAL_FAILURE: Self = Self("render.internal-failure");
277 pub const RESOURCE_SCENE_BYTES_EXCEEDED: Self = Self("resource.scene-bytes-exceeded");
279 pub const RESOURCE_ELEMENT_COUNT_EXCEEDED: Self = Self("resource.element-count-exceeded");
281 pub const RESOURCE_ASSET_COUNT_EXCEEDED: Self = Self("resource.asset-count-exceeded");
283 pub const RESOURCE_TOTAL_ASSET_BYTES_EXCEEDED: Self =
285 Self("resource.total-asset-bytes-exceeded");
286 pub const RESOURCE_RELATIONSHIP_DEPTH_EXCEEDED: Self =
288 Self("resource.relationship-depth-exceeded");
289 pub const RESOURCE_TOTAL_GEOMETRY_EXCEEDED: Self = Self("resource.total-geometry-exceeded");
291 pub const RESOURCE_TOTAL_TEXT_EXCEEDED: Self = Self("resource.total-text-exceeded");
293 pub const RESOURCE_TOTAL_DECODED_PIXELS_EXCEEDED: Self =
295 Self("resource.total-decoded-pixels-exceeded");
296 pub const RESOURCE_RASTER_SURFACE_EXCEEDED: Self = Self("resource.raster-surface-exceeded");
298 pub const RESOURCE_TOTAL_RASTERIZED_PIXELS_EXCEEDED: Self =
300 Self("resource.total-rasterized-pixels-exceeded");
301 pub const RESOURCE_SVG_DIMENSIONS_EXCEEDED: Self = Self("resource.svg-dimensions-exceeded");
303 pub const RESOURCE_PDF_PAGE_DIMENSIONS_EXCEEDED: Self =
305 Self("resource.pdf-page-dimensions-exceeded");
306 pub const RESOURCE_DIAGNOSTIC_COUNT_EXCEEDED: Self = Self("resource.diagnostic-count-exceeded");
308 pub const RESOURCE_WORKING_BYTES_EXCEEDED: Self = Self("resource.working-bytes-exceeded");
310 pub const RESOURCE_OUTPUT_BYTES_EXCEEDED: Self = Self("resource.output-bytes-exceeded");
312 pub const RESOURCE_TOTAL_WORK_EXCEEDED: Self = Self("resource.total-work-exceeded");
314 pub const RESOURCE_COORDINATE_RANGE_EXCEEDED: Self = Self("resource.coordinate-range-exceeded");
316 pub const RESOURCE_MEMORY_UNAVAILABLE: Self = Self("resource.memory-unavailable");
318
319 pub const FRAME_BATCH_FOREIGN_TARGET: Self = Self("frame-batch.foreign-target");
321
322 pub const FRAME_BATCH_DUPLICATE_TARGET: Self = Self("frame-batch.duplicate-target");
324
325 pub const FRAME_BATCH_SHARED_PREPARATION_FAILED: Self =
327 Self("frame-batch.shared-preparation-failed");
328
329 pub const SCENE_INVALID_UTF8: Self = Self("scene.invalid-utf8");
331
332 pub const SCENE_INVALID_JSON: Self = Self("scene.invalid-json");
334
335 pub const SCENE_INVALID_ENVELOPE: Self = Self("scene.invalid-envelope");
337
338 pub const SCENE_UNSUPPORTED_INPUT: Self = Self("scene.unsupported-input");
341
342 pub const SCENE_INPUT_TOO_LARGE: Self = Self("scene.input-too-large");
344
345 pub const SCENE_JSON_DEPTH_EXCEEDED: Self = Self("scene.json-depth-exceeded");
347
348 pub const SCENE_JSON_STRUCTURE_EXCEEDED: Self = Self("scene.json-structure-exceeded");
350
351 pub const SCENE_STRING_TOO_LARGE: Self = Self("scene.string-too-large");
353
354 pub const SCENE_OBJECT_KEY_TOO_LARGE: Self = Self("scene.object-key-too-large");
356
357 pub const SCENE_NUMBER_TOKEN_TOO_LARGE: Self = Self("scene.number-token-too-large");
359
360 pub const SCENE_DUPLICATE_OBJECT_KEY: Self = Self("scene.duplicate-object-key");
362
363 pub const SCENE_PARSE_MEMORY_EXCEEDED: Self = Self("scene.parse-memory-exceeded");
365
366 pub const fn as_str(self) -> &'static str {
368 self.0
369 }
370}
371
372impl fmt::Display for ErrorCode {
373 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
374 formatter.write_str(self.as_str())
375 }
376}
377
378#[derive(Clone, Debug, PartialEq, Serialize)]
384#[non_exhaustive]
385#[serde(tag = "kind", rename_all = "snake_case")]
386pub enum DiagnosticDetails {
387 #[non_exhaustive]
389 ParseLimit {
390 field: &'static str,
392 configured: u64,
394 value_kind: ValueKind,
396 measurement: Measurement,
398 },
399 #[non_exhaustive]
401 RequestValue {
402 field: &'static str,
404 asset_id: Option<String>,
406 option: Option<&'static str>,
408 },
409 #[non_exhaustive]
411 Compatibility {
412 recovery: Recovery,
414 recipe_id: Option<String>,
416 },
417 #[non_exhaustive]
419 Asset {
420 asset_id: String,
422 claimed_media_type: Option<String>,
424 validated_media_type: Option<String>,
426 recovery: Recovery,
428 recipe_id: Option<String>,
430 },
431 #[non_exhaustive]
433 ResourceLimit {
434 field: &'static str,
436 configured: ResourceValue,
438 value_kind: ValueKind,
440 measurement: ResourceMeasurement,
442 recovery: Option<Recovery>,
444 },
445 #[non_exhaustive]
447 MemoryUnavailable {
448 subject_kind: String,
450 subject: String,
452 },
453 #[non_exhaustive]
455 MissingGlyph {
456 codepoint: u32,
458 recovery: Recovery,
460 },
461 #[non_exhaustive]
463 FontUnavailable {
464 resolved_family: String,
466 asset_identity: String,
468 },
469 #[non_exhaustive]
471 SvgFontSubsetFailure {
472 stage: SvgFontSubsetStage,
474 resolved_family: String,
476 asset_identity: String,
478 },
479 #[non_exhaustive]
481 Metadata {
482 component: MetadataComponent,
484 operation: MetadataOperation,
486 recovery: Recovery,
488 },
489 #[non_exhaustive]
491 NestedSource {
492 asset_chain: Vec<AssetChainEntry>,
494 paths: Vec<NestedPath>,
496 recovery: Recovery,
498 },
499 #[non_exhaustive]
501 NestedResourceLimit {
502 field: &'static str,
504 configured: ResourceValue,
506 value_kind: ValueKind,
508 measurement: ResourceMeasurement,
510 asset_chain: Vec<AssetChainEntry>,
512 paths: Vec<NestedPath>,
514 recovery: Recovery,
516 recipe_id: Option<String>,
518 },
519 #[non_exhaustive]
521 NestedAssetClaim {
522 asset_chain: Vec<AssetChainEntry>,
524 paths: Vec<NestedPath>,
526 claimed_media_type: String,
528 validated_media_type: String,
530 recovery: Recovery,
532 },
533 #[non_exhaustive]
535 PdfRasterFallback {
536 recovery: Option<Recovery>,
538 logical_bounds: Option<LogicalBounds>,
540 pixel_width: Option<u64>,
542 pixel_height: Option<u64>,
544 causes: Vec<NestedPath>,
546 },
547}
548
549impl DiagnosticDetails {
550 pub(crate) fn recovery(&self) -> Option<Recovery> {
555 match self {
556 Self::Compatibility { recovery, .. }
557 | Self::Asset { recovery, .. }
558 | Self::MissingGlyph { recovery, .. }
559 | Self::Metadata { recovery, .. }
560 | Self::NestedSource { recovery, .. }
561 | Self::NestedResourceLimit { recovery, .. }
562 | Self::NestedAssetClaim { recovery, .. } => Some(*recovery),
563 Self::ResourceLimit { recovery, .. } | Self::PdfRasterFallback { recovery, .. } => {
564 *recovery
565 }
566 Self::ParseLimit { .. }
567 | Self::RequestValue { .. }
568 | Self::MemoryUnavailable { .. }
569 | Self::FontUnavailable { .. }
570 | Self::SvgFontSubsetFailure { .. } => None,
571 }
572 }
573
574 pub(crate) fn limit_crossing(&self) -> Option<LimitCrossing> {
578 let (field, configured, value_kind, measurement) = match self {
579 Self::ParseLimit {
580 field,
581 configured,
582 value_kind,
583 measurement,
584 } => (
585 *field,
586 ResourceValue::Integer(*configured),
587 *value_kind,
588 match *measurement {
589 Measurement::Attempted { value } => Measurement::Attempted {
590 value: ResourceValue::Integer(value),
591 },
592 Measurement::AtLeast { value } => Measurement::AtLeast {
593 value: ResourceValue::Integer(value),
594 },
595 },
596 ),
597 Self::ResourceLimit {
598 field,
599 configured,
600 value_kind,
601 measurement,
602 ..
603 }
604 | Self::NestedResourceLimit {
605 field,
606 configured,
607 value_kind,
608 measurement,
609 ..
610 } => (*field, *configured, *value_kind, *measurement),
611 _ => return None,
612 };
613 Some(LimitCrossing {
614 field,
615 configured,
616 value_kind,
617 measurement,
618 })
619 }
620
621 pub(crate) fn recipe_id(&self) -> Option<&str> {
624 match self {
625 Self::Compatibility { recipe_id, .. }
626 | Self::Asset { recipe_id, .. }
627 | Self::NestedResourceLimit { recipe_id, .. } => recipe_id.as_deref(),
628 Self::ParseLimit { .. }
629 | Self::RequestValue { .. }
630 | Self::ResourceLimit { .. }
631 | Self::MemoryUnavailable { .. }
632 | Self::MissingGlyph { .. }
633 | Self::FontUnavailable { .. }
634 | Self::SvgFontSubsetFailure { .. }
635 | Self::Metadata { .. }
636 | Self::NestedSource { .. }
637 | Self::NestedAssetClaim { .. }
638 | Self::PdfRasterFallback { .. } => None,
639 }
640 }
641}
642
643#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize)]
645#[non_exhaustive]
646#[serde(rename_all = "snake_case")]
647pub enum SvgFontSubsetStage {
648 Decode,
650 Plan,
652 Sfnt,
654 Encode,
656 Validate,
658}
659
660#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize)]
662#[non_exhaustive]
663#[serde(rename_all = "snake_case")]
664pub enum Recovery {
665 Repaired,
667 Ignored,
669 Placeholder,
671 Omitted,
673 Removed,
675 Substituted,
677 Flattened,
679 Rasterized,
681 MetadataOmitted,
683}
684
685impl Recovery {
686 pub const fn as_str(self) -> &'static str {
688 match self {
689 Self::Repaired => "repaired",
690 Self::Ignored => "ignored",
691 Self::Placeholder => "placeholder",
692 Self::Omitted => "omitted",
693 Self::Removed => "removed",
694 Self::Substituted => "substituted",
695 Self::Flattened => "flattened",
696 Self::Rasterized => "rasterized",
697 Self::MetadataOmitted => "metadata_omitted",
698 }
699 }
700
701 pub(crate) fn from_manifest(recovery: &str) -> Option<Self> {
703 match recovery {
704 "repaired" => Some(Self::Repaired),
705 "ignored" => Some(Self::Ignored),
706 "placeholder" => Some(Self::Placeholder),
707 "omitted" => Some(Self::Omitted),
708 "removed" => Some(Self::Removed),
709 "substituted" => Some(Self::Substituted),
710 "flattened" => Some(Self::Flattened),
711 "rasterized" => Some(Self::Rasterized),
712 "metadata_omitted" => Some(Self::MetadataOmitted),
713 _ => None,
714 }
715 }
716}
717
718impl fmt::Display for Recovery {
719 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
720 formatter.write_str(self.as_str())
721 }
722}
723
724pub(crate) struct LimitCrossing {
727 field: &'static str,
728 configured: ResourceValue,
729 value_kind: ValueKind,
730 measurement: ResourceMeasurement,
731}
732
733impl fmt::Display for LimitCrossing {
734 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
735 let (bound, value) = match self.measurement {
736 Measurement::Attempted { value } => ("", value),
737 Measurement::AtLeast { value } => ("at least ", value),
738 };
739 let unit = match self.value_kind {
740 ValueKind::Bytes => " bytes",
741 ValueKind::Pixels => " pixels",
742 ValueKind::WorkUnits => " work units",
743 ValueKind::Depth => " levels",
744 ValueKind::Count | ValueKind::Dimension | ValueKind::Coordinate => "",
745 };
746 write!(
747 formatter,
748 "needs {bound}{}{unit}, but {} is {}",
749 HumanValue(value),
750 self.field,
751 HumanValue(self.configured)
752 )
753 }
754}
755
756struct HumanValue(ResourceValue);
757
758impl fmt::Display for HumanValue {
759 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
760 match self.0 {
761 ResourceValue::Integer(value) => value.fmt(formatter),
762 ResourceValue::Finite(value) => value.get().fmt(formatter),
763 }
764 }
765}
766
767#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize)]
769#[non_exhaustive]
770#[serde(rename_all = "snake_case")]
771pub enum ValueKind {
772 Bytes,
774 Count,
776 Depth,
778 Pixels,
780 WorkUnits,
782 Dimension,
784 Coordinate,
786}
787
788#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize)]
790#[non_exhaustive]
791#[serde(tag = "kind", rename_all = "snake_case")]
792pub enum Measurement<T = u64> {
793 Attempted {
795 value: T,
797 },
798 AtLeast {
800 value: T,
802 },
803}
804
805#[derive(Clone, Copy, Debug, PartialEq)]
807pub struct FiniteValue(f64);
808
809impl FiniteValue {
810 pub(crate) fn new(value: f64) -> Option<Self> {
812 value.is_finite().then_some(Self(value))
813 }
814
815 pub const fn get(self) -> f64 {
817 self.0
818 }
819}
820
821impl Serialize for FiniteValue {
822 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
823 where
824 S: serde::Serializer,
825 {
826 let value = self.0;
827 if value == 0.0 {
828 serializer.serialize_u8(0)
829 } else if (0.0..=9_007_199_254_740_992.0).contains(&value) && (value as u64) as f64 == value
830 {
831 serializer.serialize_u64(value as u64)
832 } else if (-9_007_199_254_740_992.0..0.0).contains(&value) && (value as i64) as f64 == value
833 {
834 serializer.serialize_i64(value as i64)
835 } else {
836 serializer.serialize_f64(value)
837 }
838 }
839}
840
841#[derive(Clone, Copy, Debug, PartialEq)]
843#[non_exhaustive]
844pub enum ResourceValue {
845 Integer(u64),
847 Finite(FiniteValue),
849}
850
851impl Serialize for ResourceValue {
852 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
853 where
854 S: serde::Serializer,
855 {
856 match *self {
857 Self::Integer(value) => serializer.serialize_u64(value),
858 Self::Finite(value) => value.serialize(serializer),
859 }
860 }
861}
862
863pub type ResourceMeasurement = Measurement<ResourceValue>;
865
866#[derive(Clone, Debug, PartialEq, Serialize)]
868pub struct AssetChainEntry {
869 pub(crate) asset_id: Option<String>,
870 pub(crate) occurrence: u64,
871}
872
873impl AssetChainEntry {
874 pub fn asset_id(&self) -> Option<&str> {
876 self.asset_id.as_deref()
877 }
878
879 pub const fn occurrence(&self) -> u64 {
881 self.occurrence
882 }
883}
884
885#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize)]
887#[non_exhaustive]
888#[serde(rename_all = "snake_case")]
889pub enum NestedSegmentKind {
890 Node,
892 Attribute,
894 CssRule,
896 CssDeclaration,
898 ResourceSlot,
900 EffectInput,
902}
903
904#[derive(Clone, Debug, PartialEq, Serialize)]
906pub struct NestedPathSegment {
907 pub(crate) kind: NestedSegmentKind,
908 pub(crate) ordinal: u64,
909 pub(crate) name: Option<String>,
910}
911
912impl NestedPathSegment {
913 pub const fn kind(&self) -> NestedSegmentKind {
915 self.kind
916 }
917
918 pub const fn ordinal(&self) -> u64 {
920 self.ordinal
921 }
922
923 pub fn name(&self) -> Option<&str> {
925 self.name.as_deref()
926 }
927}
928
929#[derive(Clone, Debug, PartialEq, Serialize)]
931pub struct NestedPath {
932 pub(crate) segments: Vec<NestedPathSegment>,
933}
934
935impl NestedPath {
936 pub fn segments(&self) -> &[NestedPathSegment] {
938 &self.segments
939 }
940}
941
942#[derive(Clone, Copy, Debug, PartialEq)]
944pub struct LogicalBounds {
945 pub(crate) x: FiniteValue,
946 pub(crate) y: FiniteValue,
947 pub(crate) width: FiniteValue,
948 pub(crate) height: FiniteValue,
949}
950
951impl Serialize for LogicalBounds {
952 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
953 where
954 S: serde::Serializer,
955 {
956 let mut bounds = serializer.serialize_struct("LogicalBounds", 4)?;
957 bounds.serialize_field("x", &self.x)?;
958 bounds.serialize_field("y", &self.y)?;
959 bounds.serialize_field("width", &self.width)?;
960 bounds.serialize_field("height", &self.height)?;
961 bounds.end()
962 }
963}
964
965impl LogicalBounds {
966 pub const fn x(&self) -> f64 {
968 self.x.get()
969 }
970
971 pub const fn y(&self) -> f64 {
973 self.y.get()
974 }
975
976 pub const fn width(&self) -> f64 {
978 self.width.get()
979 }
980
981 pub const fn height(&self) -> f64 {
983 self.height.get()
984 }
985}
986
987#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize)]
989#[non_exhaustive]
990#[serde(rename_all = "snake_case")]
991pub enum MetadataComponent {
992 Provenance,
994 DiagnosticSummary,
996 EmbeddedScene,
998 Manifest,
1000}
1001
1002#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize)]
1004#[non_exhaustive]
1005#[serde(rename_all = "snake_case")]
1006pub enum MetadataOperation {
1007 Serialization,
1009 Compression,
1011 Embedding,
1013}
1014
1015#[derive(Clone, Debug, PartialEq, Serialize)]
1017pub struct ErrorDescriptor {
1018 family: ErrorFamily,
1019 category: Option<RenderErrorCategory>,
1020 code: ErrorCode,
1021 message: String,
1022 format: Option<OutputFormat>,
1023 element_id: Option<String>,
1024 json_pointer: Option<String>,
1025 details: Option<DiagnosticDetails>,
1026}
1027
1028impl ErrorDescriptor {
1029 pub(crate) fn scene_parse(
1030 code: ErrorCode,
1031 message: String,
1032 json_pointer: Option<String>,
1033 details: Option<DiagnosticDetails>,
1034 ) -> Self {
1035 Self {
1036 family: ErrorFamily::SceneParse,
1037 category: None,
1038 code,
1039 message,
1040 format: None,
1041 element_id: None,
1042 json_pointer,
1043 details,
1044 }
1045 }
1046
1047 pub(crate) fn request(code: ErrorCode, message: String, details: DiagnosticDetails) -> Self {
1048 Self {
1049 family: ErrorFamily::Request,
1050 category: None,
1051 code,
1052 message,
1053 format: None,
1054 element_id: None,
1055 json_pointer: None,
1056 details: Some(details),
1057 }
1058 }
1059
1060 pub(crate) fn render(
1061 category: RenderErrorCategory,
1062 code: ErrorCode,
1063 message: String,
1064 format: OutputFormat,
1065 element_id: Option<String>,
1066 json_pointer: String,
1067 details: Option<DiagnosticDetails>,
1068 ) -> Self {
1069 Self {
1070 family: ErrorFamily::Render,
1071 category: Some(category),
1072 code,
1073 message,
1074 format: Some(format),
1075 element_id,
1076 json_pointer: Some(json_pointer),
1077 details,
1078 }
1079 }
1080
1081 pub(crate) fn frame_batch(code: ErrorCode, message: String) -> Self {
1082 Self {
1083 family: ErrorFamily::FrameBatch,
1084 category: None,
1085 code,
1086 message,
1087 format: None,
1088 element_id: None,
1089 json_pointer: None,
1090 details: None,
1091 }
1092 }
1093
1094 pub const fn family(&self) -> ErrorFamily {
1096 self.family
1097 }
1098
1099 pub const fn category(&self) -> Option<RenderErrorCategory> {
1101 self.category
1102 }
1103
1104 pub const fn code(&self) -> ErrorCode {
1106 self.code
1107 }
1108
1109 pub fn message(&self) -> &str {
1111 &self.message
1112 }
1113
1114 pub const fn format(&self) -> Option<OutputFormat> {
1116 self.format
1117 }
1118
1119 pub fn element_id(&self) -> Option<&str> {
1121 self.element_id.as_deref()
1122 }
1123
1124 pub fn json_pointer(&self) -> Option<&str> {
1126 self.json_pointer.as_deref()
1127 }
1128
1129 pub const fn details(&self) -> Option<&DiagnosticDetails> {
1131 self.details.as_ref()
1132 }
1133}
1134
1135#[cfg(test)]
1136mod tests {
1137 use super::*;
1138
1139 #[test]
1140 fn every_detail_kind_serializes_in_its_declared_member_order() {
1141 let finite = |value| FiniteValue::new(value).expect("test value is finite");
1142 let path = NestedPath {
1143 segments: vec![NestedPathSegment {
1144 kind: NestedSegmentKind::Node,
1145 ordinal: 1,
1146 name: None,
1147 }],
1148 };
1149 let cases = [
1150 (
1151 DiagnosticDetails::ParseLimit {
1152 field: "max_scene_bytes",
1153 configured: 67_108_864,
1154 value_kind: ValueKind::Bytes,
1155 measurement: Measurement::AtLeast { value: 67_108_865 },
1156 },
1157 concat!(
1158 "{\"kind\":\"parse_limit\",\"field\":\"max_scene_bytes\",",
1159 "\"configured\":67108864,\"value_kind\":\"bytes\",",
1160 "\"measurement\":{\"kind\":\"at_least\",\"value\":67108865}}"
1161 ),
1162 ),
1163 (
1164 DiagnosticDetails::RequestValue {
1165 field: "supplemental_assets",
1166 asset_id: Some("image-1".to_owned()),
1167 option: Some("media_type"),
1168 },
1169 concat!(
1170 "{\"kind\":\"request_value\",\"field\":\"supplemental_assets\",",
1171 "\"asset_id\":\"image-1\",\"option\":\"media_type\"}"
1172 ),
1173 ),
1174 (
1175 DiagnosticDetails::Compatibility {
1176 recovery: Recovery::Substituted,
1177 recipe_id: Some("official.embeddable-offline@sha256:d1d9".to_owned()),
1178 },
1179 concat!(
1180 "{\"kind\":\"compatibility\",\"recovery\":\"substituted\",",
1181 "\"recipe_id\":\"official.embeddable-offline@sha256:d1d9\"}"
1182 ),
1183 ),
1184 (
1185 DiagnosticDetails::Asset {
1186 asset_id: "image-1".to_owned(),
1187 claimed_media_type: Some("image/jpeg".to_owned()),
1188 validated_media_type: Some("image/png".to_owned()),
1189 recovery: Recovery::Repaired,
1190 recipe_id: None,
1191 },
1192 concat!(
1193 "{\"kind\":\"asset\",\"asset_id\":\"image-1\",",
1194 "\"claimed_media_type\":\"image/jpeg\",",
1195 "\"validated_media_type\":\"image/png\",",
1196 "\"recovery\":\"repaired\",\"recipe_id\":null}"
1197 ),
1198 ),
1199 (
1200 DiagnosticDetails::ResourceLimit {
1201 field: "max_coordinate_magnitude",
1202 configured: ResourceValue::Finite(finite(1e9)),
1203 value_kind: ValueKind::Coordinate,
1204 measurement: ResourceMeasurement::Attempted {
1205 value: ResourceValue::Finite(finite(1_000_000_000.5)),
1206 },
1207 recovery: Some(Recovery::Omitted),
1208 },
1209 concat!(
1210 "{\"kind\":\"resource_limit\",",
1211 "\"field\":\"max_coordinate_magnitude\",",
1212 "\"configured\":1000000000,\"value_kind\":\"coordinate\",",
1213 "\"measurement\":{\"kind\":\"attempted\",",
1214 "\"value\":1000000000.5},\"recovery\":\"omitted\"}"
1215 ),
1216 ),
1217 (
1218 DiagnosticDetails::MemoryUnavailable {
1219 subject_kind: "output_buffer".to_owned(),
1220 subject: "svg".to_owned(),
1221 },
1222 concat!(
1223 "{\"kind\":\"memory_unavailable\",",
1224 "\"subject_kind\":\"output_buffer\",\"subject\":\"svg\"}"
1225 ),
1226 ),
1227 (
1228 DiagnosticDetails::MissingGlyph {
1229 codepoint: 0x10ffff,
1230 recovery: Recovery::Substituted,
1231 },
1232 concat!(
1233 "{\"kind\":\"missing_glyph\",\"codepoint\":1114111,",
1234 "\"recovery\":\"substituted\"}"
1235 ),
1236 ),
1237 (
1238 DiagnosticDetails::SvgFontSubsetFailure {
1239 stage: SvgFontSubsetStage::Validate,
1240 resolved_family: "Excalifont".to_owned(),
1241 asset_identity: "fonts/Excalifont/chunk.woff2@sha256:abc".to_owned(),
1242 },
1243 concat!(
1244 "{\"kind\":\"svg_font_subset_failure\",\"stage\":\"validate\",",
1245 "\"resolved_family\":\"Excalifont\",",
1246 "\"asset_identity\":\"fonts/Excalifont/chunk.woff2@sha256:abc\"}"
1247 ),
1248 ),
1249 (
1250 DiagnosticDetails::FontUnavailable {
1251 resolved_family: "Xiaolai".to_owned(),
1252 asset_identity: "fonts/Xiaolai/chunk.woff2@sha256:abc".to_owned(),
1253 },
1254 concat!(
1255 "{\"kind\":\"font_unavailable\",\"resolved_family\":\"Xiaolai\",",
1256 "\"asset_identity\":\"fonts/Xiaolai/chunk.woff2@sha256:abc\"}"
1257 ),
1258 ),
1259 (
1260 DiagnosticDetails::Metadata {
1261 component: MetadataComponent::EmbeddedScene,
1262 operation: MetadataOperation::Compression,
1263 recovery: Recovery::MetadataOmitted,
1264 },
1265 concat!(
1266 "{\"kind\":\"metadata\",\"component\":\"embedded_scene\",",
1267 "\"operation\":\"compression\",",
1268 "\"recovery\":\"metadata_omitted\"}"
1269 ),
1270 ),
1271 (
1272 DiagnosticDetails::NestedSource {
1273 asset_chain: vec![AssetChainEntry {
1274 asset_id: Some("image-1".to_owned()),
1275 occurrence: 0,
1276 }],
1277 paths: vec![path.clone()],
1278 recovery: Recovery::Removed,
1279 },
1280 concat!(
1281 "{\"kind\":\"nested_source\",",
1282 "\"asset_chain\":[{\"asset_id\":\"image-1\",\"occurrence\":0}],",
1283 "\"paths\":[{\"segments\":[{\"kind\":\"node\",",
1284 "\"ordinal\":1,\"name\":null}]}],\"recovery\":\"removed\"}"
1285 ),
1286 ),
1287 (
1288 DiagnosticDetails::NestedResourceLimit {
1289 field: "max_nested_asset_depth",
1290 configured: ResourceValue::Integer(8),
1291 value_kind: ValueKind::Depth,
1292 measurement: ResourceMeasurement::AtLeast {
1293 value: ResourceValue::Integer(9),
1294 },
1295 asset_chain: vec![AssetChainEntry {
1296 asset_id: None,
1297 occurrence: 2,
1298 }],
1299 paths: vec![path.clone()],
1300 recovery: Recovery::Omitted,
1301 recipe_id: None,
1302 },
1303 concat!(
1304 "{\"kind\":\"nested_resource_limit\",",
1305 "\"field\":\"max_nested_asset_depth\",\"configured\":8,",
1306 "\"value_kind\":\"depth\",",
1307 "\"measurement\":{\"kind\":\"at_least\",\"value\":9},",
1308 "\"asset_chain\":[{\"asset_id\":null,\"occurrence\":2}],",
1309 "\"paths\":[{\"segments\":[{\"kind\":\"node\",",
1310 "\"ordinal\":1,\"name\":null}]}],\"recovery\":\"omitted\",",
1311 "\"recipe_id\":null}"
1312 ),
1313 ),
1314 (
1315 DiagnosticDetails::NestedAssetClaim {
1316 asset_chain: vec![AssetChainEntry {
1317 asset_id: Some("image-1".to_owned()),
1318 occurrence: 0,
1319 }],
1320 paths: vec![path.clone()],
1321 claimed_media_type: "image/jpeg".to_owned(),
1322 validated_media_type: "image/png".to_owned(),
1323 recovery: Recovery::Repaired,
1324 },
1325 concat!(
1326 "{\"kind\":\"nested_asset_claim\",",
1327 "\"asset_chain\":[{\"asset_id\":\"image-1\",\"occurrence\":0}],",
1328 "\"paths\":[{\"segments\":[{\"kind\":\"node\",",
1329 "\"ordinal\":1,\"name\":null}]}],",
1330 "\"claimed_media_type\":\"image/jpeg\",",
1331 "\"validated_media_type\":\"image/png\",\"recovery\":\"repaired\"}"
1332 ),
1333 ),
1334 (
1335 DiagnosticDetails::PdfRasterFallback {
1336 recovery: None,
1337 logical_bounds: Some(LogicalBounds {
1338 x: finite(-0.0),
1339 y: finite(1.5),
1340 width: finite(10.0),
1341 height: finite(20.0),
1342 }),
1343 pixel_width: None,
1344 pixel_height: None,
1345 causes: vec![path],
1346 },
1347 concat!(
1348 "{\"kind\":\"pdf_raster_fallback\",\"recovery\":null,",
1349 "\"logical_bounds\":{\"x\":0,\"y\":1.5,",
1350 "\"width\":10,\"height\":20},\"pixel_width\":null,",
1351 "\"pixel_height\":null,\"causes\":[{\"segments\":[{",
1352 "\"kind\":\"node\",\"ordinal\":1,\"name\":null}]}]}"
1353 ),
1354 ),
1355 ];
1356
1357 let mut kinds = Vec::new();
1358 for (details, expected) in cases {
1359 let kind = match &details {
1362 DiagnosticDetails::ParseLimit { .. } => "parse_limit",
1363 DiagnosticDetails::RequestValue { .. } => "request_value",
1364 DiagnosticDetails::Compatibility { .. } => "compatibility",
1365 DiagnosticDetails::Asset { .. } => "asset",
1366 DiagnosticDetails::ResourceLimit { .. } => "resource_limit",
1367 DiagnosticDetails::MemoryUnavailable { .. } => "memory_unavailable",
1368 DiagnosticDetails::MissingGlyph { .. } => "missing_glyph",
1369 DiagnosticDetails::FontUnavailable { .. } => "font_unavailable",
1370 DiagnosticDetails::SvgFontSubsetFailure { .. } => "svg_font_subset_failure",
1371 DiagnosticDetails::Metadata { .. } => "metadata",
1372 DiagnosticDetails::NestedSource { .. } => "nested_source",
1373 DiagnosticDetails::NestedResourceLimit { .. } => "nested_resource_limit",
1374 DiagnosticDetails::NestedAssetClaim { .. } => "nested_asset_claim",
1375 DiagnosticDetails::PdfRasterFallback { .. } => "pdf_raster_fallback",
1376 };
1377 kinds.push(kind);
1378 assert_eq!(
1379 serde_json::to_string(&details).expect("serialize schema-1 details"),
1380 expected
1381 );
1382 }
1383 kinds.sort_unstable();
1384 kinds.dedup();
1385 assert_eq!(kinds.len(), 14, "one case for every detail kind");
1386 }
1387
1388 #[test]
1389 fn stable_numeric_values_reject_non_finite_input() {
1390 assert_eq!(FiniteValue::new(f64::NAN), None);
1391 assert_eq!(FiniteValue::new(f64::INFINITY), None);
1392 assert_eq!(FiniteValue::new(f64::NEG_INFINITY), None);
1393 assert_eq!(FiniteValue::new(-0.0).map(FiniteValue::get), Some(-0.0));
1394 }
1395}