-
Notifications
You must be signed in to change notification settings - Fork 10
fix: annotation behavioral fixes and IO stability #175
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
| @@ -1,4 +1,19 @@ | ||||||
| //ASAP XML not yet fully tested, does not render well all objects, problem with hierarchies | ||||||
| const ASAP_TYPE_BY_FACTORY = { | ||||||
| rect: "Rectangle", | ||||||
| polygon: "Polygon", | ||||||
| multipolygon: "Polygon", | ||||||
| ellipse: "Polygon", | ||||||
| polyline: "Spline", | ||||||
| line: "Spline", | ||||||
| ruler: "Spline", | ||||||
| point: "Dot", | ||||||
| text: "Dot", | ||||||
| }; | ||||||
| const ASAP_LOSSY_FACTORIES = new Set([ | ||||||
| "multipolygon", "ellipse", "polyline", "line", "ruler", "text" | ||||||
| ]); | ||||||
|
|
||||||
| OSDAnnotations.Convertor.register("asap-xml", class extends OSDAnnotations.Convertor.IConvertor { | ||||||
| static title = 'ASAP-XML Annotations'; | ||||||
| static description = 'ASAP-compatible XML Annotations Format'; | ||||||
|
|
@@ -52,6 +67,7 @@ OSDAnnotations.Convertor.register("asap-xml", class extends OSDAnnotations.Conve | |||||
| const result = {}; | ||||||
|
|
||||||
| let wasError = ''; | ||||||
| const lossyTypes = new Set(); | ||||||
|
|
||||||
| let doc = document.implementation.createDocument("", "", null); | ||||||
| const presetsIdSet = new Set(); | ||||||
|
|
@@ -70,9 +86,7 @@ OSDAnnotations.Convertor.register("asap-xml", class extends OSDAnnotations.Conve | |||||
|
|
||||||
| let factory = this.context.getAnnotationObjectFactory(obj.factoryID); | ||||||
| if (factory) { | ||||||
| // Todo some better reporting mechanism | ||||||
| if (factory.factoryID === "multipolygon") { | ||||||
| wasError = 'ASAP XML Does not support multipolygons - saved as polygon.'; | ||||||
| coordinates = this.context.polygonFactory.toPointArray({points: obj.points[0]}, | ||||||
| OSDAnnotations.AnnotationObjectFactory.withArrayPoint); | ||||||
| } else { | ||||||
|
|
@@ -84,7 +98,21 @@ OSDAnnotations.Convertor.register("asap-xml", class extends OSDAnnotations.Conve | |||||
| continue; | ||||||
| } | ||||||
| } | ||||||
| xml_annotation.setAttribute("Type", "Polygon"); | ||||||
| const asapType = ASAP_TYPE_BY_FACTORY[factory?.factoryID] || "Polygon"; | ||||||
| xml_annotation.setAttribute("Type", asapType); | ||||||
| // Per-annotation factoryID (custom attribute, ASAP ignores unknown attrs). | ||||||
| // Required because a preset's FactoryID is a default, but its annotations may differ. | ||||||
| if (factory?.factoryID) { | ||||||
| xml_annotation.setAttribute("xopatFactoryID", factory.factoryID); | ||||||
| } | ||||||
| if (factory && ASAP_LOSSY_FACTORIES.has(factory.factoryID)) { | ||||||
| lossyTypes.add(factory.factoryID); | ||||||
| } | ||||||
| if (factory?.factoryID === "ruler") { | ||||||
| const inner = obj._objects || obj.objects; | ||||||
| const txt = inner?.[1]?.text; | ||||||
| if (txt) xml_annotation.setAttribute("Description", txt); | ||||||
| } | ||||||
|
|
||||||
| //todo attr name could be set from preset | ||||||
| xml_annotation.setAttribute("Name", "Annotation " + i); | ||||||
|
|
@@ -150,9 +178,14 @@ OSDAnnotations.Convertor.register("asap-xml", class extends OSDAnnotations.Conve | |||||
| //todo check for consitency presetsIdSet? | ||||||
| } | ||||||
|
|
||||||
| if (lossyTypes.size) { | ||||||
| const lossyMsg = `ASAP-XML is lossy for: ${[...lossyTypes].join(", ")}. ` | ||||||
| + `Re-import in xopat preserves class via preset FactoryID; other tools see only the geometry.`; | ||||||
| wasError = wasError ? wasError + "\n" + lossyMsg : lossyMsg; | ||||||
| } | ||||||
| // Todo - create some unified checking mechanism that reports on export issues | ||||||
| if (wasError) { | ||||||
| Dialogs.show(wasError, 15000, Dialogs.MSG_ERR); | ||||||
| Dialogs.show(wasError, 15000, Dialogs.MSG_WARN); | ||||||
| } | ||||||
|
|
||||||
| return result; | ||||||
|
|
@@ -210,22 +243,56 @@ OSDAnnotations.Convertor.register("asap-xml", class extends OSDAnnotations.Conve | |||||
| for (const coordElem of coords.getElementsByTagName("Coordinate")) { | ||||||
| const index = Number.parseInt(coordElem.getAttribute("Order")); | ||||||
| pointArray[index] = { | ||||||
| x: Number.parseInt(coordElem.getAttribute("X")), | ||||||
| y: Number.parseInt(coordElem.getAttribute("Y")) | ||||||
| x: Number.parseFloat(coordElem.getAttribute("X")), | ||||||
| y: Number.parseFloat(coordElem.getAttribute("Y")) | ||||||
| } | ||||||
| } | ||||||
|
|
||||||
| const presetID = elem.getAttribute("PartOfGroup"); | ||||||
|
|
||||||
| //todo support: Dot, Rectangle, Polygon, Spline, and PointSet by implementation of general annotation structure | ||||||
| //todo attr name could be set as category custom meta | ||||||
| annotations.push({ | ||||||
| type: "polygon", | ||||||
| points: pointArray, | ||||||
| presetID: presetID, | ||||||
| factoryID: "polygon", | ||||||
| color: elem.getAttribute("Color") || undefined | ||||||
| }); | ||||||
| const rawPresetID = elem.getAttribute("PartOfGroup"); | ||||||
| const presetID = Number.parseInt(rawPresetID) || rawPresetID; | ||||||
| const preset = presets[presetID]; | ||||||
| // Prefer per-annotation factoryID (custom attribute) over the preset's default factoryID: | ||||||
| // a preset's FactoryID is just the default; its annotations may be of other types. | ||||||
| const xopatFacID = elem.getAttribute("xopatFactoryID"); | ||||||
| let facID = xopatFacID || preset?.factoryID || "polygon"; | ||||||
| // ASAP-XML drops multipolygon rings on export (only outer ring of first polygon is kept); | ||||||
| // decode that single ring as a polygon to avoid Multipolygon.fromPointArray misinterpreting flat points. | ||||||
| if (facID === "multipolygon") facID = "polygon"; | ||||||
| const factory = this.context.getAnnotationObjectFactory(facID); | ||||||
| const color = elem.getAttribute("Color") || preset?.color || undefined; | ||||||
|
|
||||||
| let pushed = false; | ||||||
| if (factory && typeof factory.fromPointArray === "function" && | ||||||
| typeof factory.create === "function") { | ||||||
| try { | ||||||
| const arrPts = pointArray.map(p => [p.x, p.y]); | ||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. If Filter out empty/undefined slots to ensure a dense array of coordinates is passed to the factory.
Suggested change
|
||||||
| const params = factory.fromPointArray(arrPts, ([x, y]) => ({ x, y })); | ||||||
| const opts = { | ||||||
| color, | ||||||
| presetID, | ||||||
| factoryID: facID, | ||||||
| ...(this.context.presets.getCommonProperties?.() || {}), | ||||||
| }; | ||||||
| const obj = factory.create(params, opts); | ||||||
| if (facID === "ruler") { | ||||||
| const desc = elem.getAttribute("Description"); | ||||||
| if (desc && obj?._objects?.[1]) obj._objects[1].set({ text: desc }); | ||||||
| } | ||||||
| annotations.push(obj); | ||||||
| pushed = true; | ||||||
| } catch (e) { | ||||||
| console.warn("ASAP-XML decode: factory reconstruction failed, falling back to polygon", facID, e); | ||||||
| } | ||||||
| } | ||||||
| if (!pushed) { | ||||||
| annotations.push({ | ||||||
| type: "polygon", | ||||||
| points: pointArray, | ||||||
| presetID, | ||||||
| factoryID: "polygon", | ||||||
| color | ||||||
| }); | ||||||
| } | ||||||
| } | ||||||
|
|
||||||
| return { | ||||||
|
|
||||||
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
|
|
@@ -76,6 +76,7 @@ OSDAnnotations.Convertor.register("geo-json", class extends OSDAnnotations.Conve | |||||
| return res; | ||||||
| }, | ||||||
| "polyline": (object) => this._asGEOJsonFeature(object, "LineString", ["points"], false), | ||||||
| "line": (object) => this._asGEOJsonFeature(object, "LineString", [], false), | ||||||
| "point": (object) => { | ||||||
| object = this._asGEOJsonFeature(object, "Point"); | ||||||
| object.geometry.coordinates = object.geometry.coordinates[0] || []; | ||||||
|
|
@@ -89,12 +90,15 @@ OSDAnnotations.Convertor.register("geo-json", class extends OSDAnnotations.Conve | |||||
| "ruler": (object) => { | ||||||
| const factory = this.context.getAnnotationObjectFactory(object.factoryID); | ||||||
| const converter = OSDAnnotations.AnnotationObjectFactory.withArrayPoint; | ||||||
| object._objects = object.objects; // todo ugly, factory works with live objects | ||||||
| const props = factory.copyNecessaryProperties(object, [], true); | ||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. If
Suggested change
|
||||||
| props.text = object._objects?.[1]?.text; | ||||||
| return { | ||||||
| geometry: { | ||||||
| type: "LineString", | ||||||
| coordinates: factory?.toPointArray(object, converter, fabric.Object.NUM_FRACTION_DIGITS) | ||||||
| }, | ||||||
| properties: factory.copyNecessaryProperties(object, [], true) | ||||||
| properties: props | ||||||
| }; | ||||||
| }, | ||||||
| }; | ||||||
|
|
@@ -110,6 +114,12 @@ OSDAnnotations.Convertor.register("geo-json", class extends OSDAnnotations.Conve | |||||
| (object, geometry) => object.points = geometry.map(ring => this._toNativeRing(ring))), | ||||||
| "polyline": (object) => this._getAsNativeObject(object, | ||||||
| (object, geometry) => object.points = this._toNativeRing(geometry, false)), | ||||||
| "line": (object) => { | ||||||
| const factory = this.context.getAnnotationObjectFactory("line"); | ||||||
| const params = factory.fromPointArray(object.geometry.coordinates, ([x, y]) => ({ x, y })); | ||||||
| const opts = $.extend({}, this.context.presets.getCommonProperties(), object.properties); | ||||||
| return factory.create(params, opts); | ||||||
|
Comment on lines
+118
to
+121
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. If the const factory = this.context.getAnnotationObjectFactory("line");
if (!factory) return null;
const params = factory.fromPointArray(object.geometry.coordinates, ([x, y]) => ({ x, y }));
const opts = $.extend({}, this.context.presets.getCommonProperties(), object.properties);
return factory.create(params, opts); |
||||||
| }, | ||||||
| "point": (object) => this._getAsNativeObject(object, (object, geometry) => { | ||||||
| //todo not necessary? left/top are already probably present in props | ||||||
| object.left = geometry[0]; | ||||||
|
|
@@ -122,7 +132,16 @@ OSDAnnotations.Convertor.register("geo-json", class extends OSDAnnotations.Conve | |||||
| object.top = geometry[1]; | ||||||
| return object; | ||||||
| }), | ||||||
| "ruler": (object) => this._getAsNativeObject(object), | ||||||
| "ruler": (object) => { | ||||||
| const factory = this.context.getAnnotationObjectFactory("ruler"); | ||||||
| const params = factory.fromPointArray(object.geometry.coordinates, ([x, y]) => ({ x, y })); | ||||||
| const opts = $.extend({}, this.context.presets.getCommonProperties(), object.properties); | ||||||
| const obj = factory.create(params, opts); | ||||||
|
Comment on lines
+136
to
+139
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. If the const factory = this.context.getAnnotationObjectFactory("ruler");
if (!factory) return null;
const params = factory.fromPointArray(object.geometry.coordinates, ([x, y]) => ({ x, y }));
const opts = $.extend({}, this.context.presets.getCommonProperties(), object.properties);
const obj = factory.create(params, opts); |
||||||
| if (object.properties?.text && obj?._objects?.[1]) { | ||||||
| obj._objects[1].set({ text: object.properties.text }); | ||||||
| } | ||||||
| return obj; | ||||||
| }, | ||||||
| }; | ||||||
|
|
||||||
| // todo support unpacking like qupath does | ||||||
|
|
||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Passing the entire fabric object
objas theoptionsparameter tofactory.configureis highly inefficient and dangerous. InRuler.configure,$.extend({}, options, ...)andObject.assign({}, options)are called, which will attempt to clone/copy all properties of the fabric instance (including circular references, canvas references, etc.). This can lead to severe performance degradation, memory bloat, or runtime errors.Instead, extract and pass only the necessary properties as a plain object.