diff --git a/examples/al-hips-local.html b/examples/al-hips-local.html
index c7e40d18..5ca30de8 100644
--- a/examples/al-hips-local.html
+++ b/examples/al-hips-local.html
@@ -14,7 +14,7 @@
A.init.then(() => {
aladin = A.aladin('#aladin-lite-div', {target: "05 40 59.12 -02 27 04.1", fov: 2});
- let survey = aladin.createImageSurvey('DSS2 local', "local hips", "hips/CDS_P_DSS2_color");
+ let survey = aladin.createImageSurvey('DSS2 local', "local hips", "./hips/CDS_P_DSS2_color");
aladin.setBaseImageLayer(survey);
});
diff --git a/examples/al-image-with-AVM-tags.html b/examples/al-image-with-AVM-tags.html
new file mode 100644
index 00000000..a219001d
--- /dev/null
+++ b/examples/al-image-with-AVM-tags.html
@@ -0,0 +1,28 @@
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/examples/al-image-with-WCS.html b/examples/al-image-with-WCS.html
index 1ae709b8..2520c405 100644
--- a/examples/al-image-with-WCS.html
+++ b/examples/al-image-with-WCS.html
@@ -13,7 +13,6 @@
"https://nova.astrometry.net/image/25038473?filename=M61.jpg",
{
name: "M61",
- imgFormat: 'jpeg',
wcs: {
NAXIS: 0, // Minimal header
CTYPE1: 'RA---TAN', // TAN (gnomic) projection
diff --git a/src/core/src/app.rs b/src/core/src/app.rs
index a03ddd94..c6723db7 100644
--- a/src/core/src/app.rs
+++ b/src/core/src/app.rs
@@ -1115,7 +1115,6 @@ impl App {
pub(crate) fn add_image_fits(
&mut self,
- id: String,
stream: web_sys::ReadableStream,
meta: ImageMetadata,
layer: String,
@@ -1245,8 +1244,9 @@ impl App {
} else {
let fits = ImageLayer {
images,
+ id: layer.clone(),
+
layer,
- id,
meta,
};
@@ -1301,6 +1301,7 @@ impl App {
) -> Result<(), JsValue> {
let old_meta = self.layers.get_layer_cfg(&layer)?;
// Set the new meta
+ // keep the old meta data
let new_img_fmt = meta.img_format;
self.layers
.set_layer_cfg(layer.clone(), meta, &mut self.camera, &self.projection)?;
diff --git a/src/core/src/lib.rs b/src/core/src/lib.rs
index 4da56537..85be3dff 100644
--- a/src/core/src/lib.rs
+++ b/src/core/src/lib.rs
@@ -382,21 +382,19 @@ impl WebClient {
#[wasm_bindgen(js_name = addImageFITS)]
pub fn add_image_fits(
&mut self,
- id: String,
stream: web_sys::ReadableStream,
cfg: JsValue,
layer: String,
) -> Result {
let cfg: ImageMetadata = serde_wasm_bindgen::from_value(cfg)?;
- //let wcs: Option = serde_wasm_bindgen::from_value(wcs)?;
- self.app.add_image_fits(id, stream, cfg, layer)
+ self.app.add_image_fits(stream, cfg, layer)
}
#[wasm_bindgen(js_name = addImageWithWCS)]
pub fn add_image_with_wcs(
&mut self,
- data: web_sys::ReadableStream,
+ stream: web_sys::ReadableStream,
wcs: JsValue,
cfg: JsValue,
layer: String,
@@ -406,7 +404,7 @@ impl WebClient {
let wcs_params: WCSParams = serde_wasm_bindgen::from_value(wcs)?;
let wcs = WCS::new(&wcs_params).map_err(|e| JsValue::from_str(&format!("{:?}", e)))?;
- self.app.add_image_from_blob_and_wcs(layer, data, wcs, cfg)
+ self.app.add_image_from_blob_and_wcs(layer, stream, wcs, cfg)
}
#[wasm_bindgen(js_name = removeLayer)]
diff --git a/src/js/Image.js b/src/js/Image.js
index 0e707bc7..c89c6cd9 100644
--- a/src/js/Image.js
+++ b/src/js/Image.js
@@ -28,6 +28,9 @@
import { ALEvent } from "./events/ALEvent.js";
import { ColorCfg } from "./ColorCfg.js";
import { Aladin } from "./Aladin.js";
+import { Utils } from "./Utils";
+import { AVM } from "./libs/avm.js";
+
/**
* @typedef {Object} ImageOptions
@@ -48,7 +51,7 @@ import { Aladin } from "./Aladin.js";
* @property {number} [contrast=0.0] - The contrast value for the color configuration.
* @property {Object} [wcs] - an object describing the WCS of the image. In case of a fits image
* this property will be ignored as the WCS taken will be the one present in the fits file.
- * @property {number} [imgFormat='fits'] - The image format of the image to load.
+ * @property {string} [imgFormat] - Optional image format. Giving it will prevent the auto extension determination algorithm to be triggered. Possible values are 'jpeg', 'png' or 'fits'.
*
* @example
*
@@ -56,7 +59,6 @@ import { Aladin } from "./Aladin.js";
* "https://nova.astrometry.net/image/25038473?filename=M61.jpg",
* {
* name: "M61",
- * imgFormat: 'jpeg',
* wcs: {
* NAXIS: 0, // Minimal header
* CTYPE1: 'RA---TAN', // TAN (gnomic) projection + SIP distortions
@@ -104,8 +106,8 @@ export let Image = (function () {
this.url = url;
this.id = url;
this.name = (options && options.name) || this.url;
- this.imgFormat = (options && options.imgFormat) || "fits";
- this.formats = [this.imgFormat];
+ this.imgFormat = options && options.imgFormat;
+ //this.formats = [this.imgFormat];
// callbacks
this.successCallback = options && options.successCallback;
this.errorCallback = options && options.errorCallback;
@@ -116,7 +118,7 @@ export let Image = (function () {
}*/
this.colorCfg = new ColorCfg(options);
- this.options = options;
+ this.options = options || {};
let self = this;
@@ -219,92 +221,185 @@ export let Image = (function () {
}
};
+ Image.prototype._addFITS = function(layer) {
+ let self = this;
+
+ return Utils.fetch({
+ url: this.url,
+ dataType: 'readableStream',
+ success: (stream) => {
+ return self.view.wasm.addImageFITS(
+ stream,
+ {
+ ...self.colorCfg.get(),
+ longitudeReversed: false,
+ imgFormat: 'fits',
+ },
+ layer
+ );
+ }
+ })
+ .then((imageParams) => {
+ self.imgFormat = 'fits';
+
+ return Promise.resolve(imageParams);
+ });
+ }
+
+ Image.prototype._addJPGOrPNG = function(layer) {
+ let self = this;
+ let img = document.createElement('img');
+
+ return new Promise((resolve, reject) => {
+ img.src = this.url;
+ img.crossOrigin = "Anonymous";
+ img.onload = () => {
+ const img2Blob = () => {
+ var canvas = document.createElement("canvas");
+
+ canvas.width = img.width;
+ canvas.height = img.height;
+
+ // Copy the image contents to the canvas
+ var ctx = canvas.getContext("2d");
+ ctx.drawImage(img, 0, 0, img.width, img.height);
+
+ const imageData = ctx.getImageData(0, 0, img.width, img.height);
+
+ const blob = new Blob([imageData.data]);
+ const stream = blob.stream(1024);
+
+ console.log(self.options, stream)
+
+
+ resolve(stream)
+ };
+
+ if (!self.options.wcs) {
+ /* look for avm tags if no wcs is given */
+ let avm = new AVM(img);
+ console.log(img.complete)
+ avm.load((obj) => {
+ // obj contains the following information:
+ // obj.id (string) = The ID provided for the image
+ // obj.img (object) = The image object
+ // obj.xmp (string) = The raw XMP header
+ // obj.avmdata (Boolean) = If AVM tags have been loaded
+ // obj.tags (object) = An array containing all the loaded tags e.g. obj.tags['Headline']
+ console.log(obj)
+
+ if (obj.avmdata) {
+ let wcs = {};
+ wcs.CTYPE1 = obj.tags['Spatial.CoordinateFrame'] === 'ICRS' ? 'RA---' : 'GLON-';
+ wcs.CTYPE1 += obj.tags['Spatial.CoordsystemProjection'];
+ wcs.CTYPE2 = obj.tags['Spatial.CoordinateFrame'] === 'ICRS' ? 'DEC--' : 'GLAT-';
+ wcs.CTYPE2 += obj.tags['Spatial.CoordsystemProjection'];
+
+ if (obj.tags['Spatial.Equinox'])
+ wcs.EQUINOX = +obj.tags['Spatial.Equinox'];
+
+ wcs.NAXIS1 = +obj.tags['Spatial.ReferenceDimension'][0];
+ wcs.NAXIS2 = +obj.tags['Spatial.ReferenceDimension'][1];
+
+ wcs.CDELT1 = +obj.tags['Spatial.Scale'][0];
+ wcs.CDELT2 = +obj.tags['Spatial.Scale'][1];
+
+ wcs.CRPIX1 = +obj.tags['Spatial.ReferencePixel'][0];
+ wcs.CRPIX2 = +obj.tags['Spatial.ReferencePixel'][1];
+
+ wcs.CRVAL1 = +obj.tags['Spatial.ReferenceValue'][0];
+ wcs.CRVAL2 = +obj.tags['Spatial.ReferenceValue'][1];
+
+ if (obj.tags['Spatial.Rotation'] !== undefined)
+ wcs.CROTA2 = +obj.tags['Spatial.Rotation'];
+
+ self.options.wcs = wcs;
+
+ img2Blob()
+ } else {
+ // no tags found
+ reject('No AVM tags have been found in the image')
+ return;
+ }
+
+ })
+ } else {
+ img2Blob()
+ }
+ }
+
+ let proxyUsed = false;
+ img.onerror = () => {
+ // use proxy
+ if (proxyUsed) {
+ reject('Error parsing img ' + self.url)
+ return;
+ }
+
+ proxyUsed = true;
+ img.src = Aladin.JSONP_PROXY + '?url=' + self.url;
+ }
+ })
+ .then((readableStream) => {
+ let wcs = self.options && self.options.wcs;
+ wcs.NAXIS1 = wcs.NAXIS1 || img.width;
+ wcs.NAXIS2 = wcs.NAXIS2 || img.height;
+
+ return self.view.wasm
+ .addImageWithWCS(
+ readableStream,
+ wcs,
+ {
+ ...self.colorCfg.get(),
+ longitudeReversed: false,
+ imgFormat: 'jpeg',
+ },
+ layer
+ )
+ })
+ .then((imageParams) => {
+ self.imgFormat = 'jpeg';
+ return Promise.resolve(imageParams);
+ })
+ .finally(() => {
+ img.remove();
+ });
+ }
+
Image.prototype.add = function (layer) {
this.layer = layer;
let self = this;
-
let promise;
+
if (this.imgFormat === 'fits') {
- let id = this.url;
- promise = fetch(this.url)
- .then((resp) => resp.body)
- .then((readableStream) => {
- return self.view.wasm
- .addImageFITS(
- id,
- readableStream,
- {
- ...self.colorCfg.get(),
- longitudeReversed: false,
- imgFormat: self.imgFormat,
- },
- layer
- )
+ promise = this._addFITS(layer)
+ .catch(e => {
+ console.error(`Image located at ${this.url} could not be parsed as fits file. Is the imgFormat specified correct?`)
+ return Promise.reject(e)
})
- } else if (this.imgFormat === 'jpg' || this.imgFormat === 'jpeg') {
- let img = document.createElement('img');
-
- promise =
- new Promise((resolve, reject) => {
- img.src = this.url;
- img.crossOrigin = "Anonymous";
- img.onload = () => {
- var canvas = document.createElement("canvas");
-
- canvas.width = img.width;
- canvas.height = img.height;
-
- // Copy the image contents to the canvas
- var ctx = canvas.getContext("2d");
- ctx.drawImage(img, 0, 0, img.width, img.height);
-
- const imageData = ctx.getImageData(0, 0, img.width, img.height);
-
- const blob = new Blob([imageData.data]);
- const stream = blob.stream(1024);
-
- resolve(stream)
- }
-
- let proxyUsed = false;
- img.onerror = () => {
- // use proxy
- if (proxyUsed) {
- reject('Error parsing img ' + self.url)
- return;
- }
-
- proxyUsed = true;
- img.src = Aladin.JSONP_PROXY + '?url=' + self.url;
- }
- })
- .then((readableStream) => {
- let wcs = self.options && self.options.wcs;
- wcs.NAXIS1 = wcs.NAXIS1 || img.width;
- wcs.NAXIS2 = wcs.NAXIS2 || img.height;
-
- return self.view.wasm
- .addImageWithWCS(
- readableStream,
- wcs,
- {
- ...self.colorCfg.get(),
- longitudeReversed: false,
- imgFormat: self.imgFormat,
- },
- layer
- )
- })
- .finally(() => {
- img.remove();
+ } else if (this.imgFormat === 'jpeg' || this.imgFormat === 'png') {
+ promise = this._addJPGOrPNG(layer)
+ .catch(e => {
+ console.error(`Image located at ${this.url} could not be parsed as a ${this.imgFormat} file. Is the imgFormat specified correct?`);
+ return Promise.reject(e)
})
} else {
- console.warn(`Image format: ${this.imgFormat} not supported`);
- promise = Promise.reject();
- };
+ // imgformat not defined we will try first supposing it is a fits file and then use the jpg heuristic
+ promise = this._addFITS(layer)
+ .catch(_ => {
+ console.warn(`Image located at ${self.url} could not be parsed as fits file. Try as a jpg/png image...`)
+ return self._addJPGOrPNG(layer)
+ .catch(e => {
+ console.error(`Image located at ${self.url} could not be parsed as jpg/png image file. Aborting...`)
+ return Promise.reject(e);
+ })
+ })
+ }
promise = promise.then((imageParams) => {
+ self.formats = [self.imgFormat];
+
// There is at least one entry in imageParams
self.added = true;
self.setView(self.view);
diff --git a/src/js/Utils.ts b/src/js/Utils.ts
index a414639a..412518b8 100644
--- a/src/js/Utils.ts
+++ b/src/js/Utils.ts
@@ -381,6 +381,8 @@ Utils.fetch = function(params) {
return resp.json()
} else if (params.dataType && params.dataType.includes('blob')) {
return resp.blob()
+ } else if (params.dataType && params.dataType.includes('readableStream')) {
+ return resp.body;
} else {
return resp.text()
}
diff --git a/src/js/libs/avm.js b/src/js/libs/avm.js
new file mode 100644
index 00000000..49b9511b
--- /dev/null
+++ b/src/js/libs/avm.js
@@ -0,0 +1,256 @@
+/*
+ * Javascript AVM/XMP Reader 0.1.3
+ * Copyright (c) 2010 Stuart Lowe http://www.strudel.org.uk/
+ * Astronomy Visualization Metadata (AVM) is defined at:
+ * http://www.virtualastronomy.org/avm_metadata.php
+ *
+ * Licensed under the MPL http://www.mozilla.org/MPL/MPL-1.1.txt
+ *
+ */
+
+export let AVM = (function() {
+
+ function AVM(input) {
+ if (input instanceof HTMLImageElement) {
+ this.img = input;
+ } else {
+ // suppose that input is a string
+ this.id = (input) ? input : "";
+ this.img = { complete: false };
+ }
+
+ this.xmp = ""; // Will hold the XMP string (for test purposes)
+ this.avmdata = false;
+ this.tags = {}
+ this.AVMdefinedTags = {
+ 'Creator':'photoshop:Source',
+ 'CreatorURL':'Iptc4xmpCore:CiUrlWork',
+ 'Contact.Name':'dc:creator',
+ 'Contact.Email':'Iptc4xmpCore:CiEmailWork',
+ 'Contact.Telephone':'Iptc4xmpCore:CiTelWork',
+ 'Contact.Address':'Iptc4xmpCore:CiAdrExtadr',
+ 'Contact.City':'Iptc4xmpCore:CiAdrCity',
+ 'Contact.StateProvince':'Iptc4xmpCore:CiAdrRegion',
+ 'Contact.PostalCode':'Iptc4xmpCore:CiAdrPcode',
+ 'Contact.Country':'Iptc4xmpCore:CiAdrCtry',
+ 'Rights':'xapRights:UsageTerms',
+ 'Title':'dc:title',
+ 'Headline':'photoshop:Headline',
+ 'Description':'dc:description',
+ 'Subject.Category':'avm:Subject.Category',
+ 'Subject.Name':'dc:subject',
+ 'Distance':'avm:Distance',
+ 'Distance.Notes':'avm:Distance.Notes',
+ 'ReferenceURL':'avm:ReferenceURL',
+ 'Credit':'photoshop:Credit',
+ 'Date':'photoshop:DateCreated',
+ 'ID':'avm:ID',
+ 'Type':'avm:Type',
+ 'Image.ProductQuality':'avm:Image.ProductQuality',
+ 'Facility':'avm:Facility',
+ 'Instrument':'avm:Instrument',
+ 'Spectral.ColorAssignment':'avm:Spectral.ColorAssignment',
+ 'Spectral.Band':'avm:Spectral.Band',
+ 'Spectral.Bandpass':'avm:Spectral.Bandpass',
+ 'Spectral.CentralWavelength':'avm:Spectral.CentralWavelength',
+ 'Spectral.Notes':'avm:Spectral.Notes',
+ 'Temporal.StartTime':'avm:Temporal.StartTime',
+ 'Temporal.IntegrationTime':'avm:Temporal.IntegrationTime',
+ 'DatasetID':'avm:DatasetID',
+ 'Spatial.CoordinateFrame':'avm:Spatial.CoordinateFrame',
+ 'Spatial.Equinox':'avm:Spatial.Equinox',
+ 'Spatial.ReferenceValue':'avm:Spatial.ReferenceValue',
+ 'Spatial.ReferenceDimension':'avm:Spatial.ReferenceDimension',
+ 'Spatial.ReferencePixel':'avm:Spatial.ReferencePixel',
+ 'Spatial.Scale':'avm:Spatial.Scale',
+ 'Spatial.Rotation':'avm:Spatial.Rotation',
+ 'Spatial.CoordsystemProjection':'avm:Spatial.CoordsystemProjection',
+ 'Spatial.Quality':'avm:Spatial.Quality',
+ 'Spatial.Notes':'avm:Spatial.Notes',
+ 'Spatial.FITSheader':'avm:Spatial.FITSheader',
+ 'Spatial.CDMatrix':'avm:Spatial.CDMatrix',
+ 'Publisher':'avm:Publisher',
+ 'PublisherID':'avm:PublisherID',
+ 'ResourceID':'avm:ResourceID',
+ 'ResourceURL':'avm:ResourceURL',
+ 'RelatedResources':'avm:RelatedResources',
+ 'MetadataDate':'avm:MetadataDate',
+ 'MetadataVersion':'avm:MetadataVersion'
+ }
+ }
+
+ AVM.prototype.load = function(fnCallback) {
+ if(!this.img && this.id) {
+ this.img = document.getElementById(this.id);
+ }
+
+ if(!this.img.complete) {
+ var _obj = this;
+ addEvent(this.img, "load",
+ function() {
+ _obj.getData(fnCallback);
+ }
+ );
+ }else{
+ this.getData(fnCallback);
+ }
+ }
+
+ AVM.prototype.getData = function(fnCallback){
+ if(!this.imageHasData()){
+ this.getImageData(this.img, fnCallback);
+ }else{
+ if(typeof fnCallback=="function") fnCallback(this);
+ }
+ return true;
+ }
+
+ AVM.prototype.getImageData = function(oImg, fnCallback) {
+ var _obj = this;
+ let reqwst = new Request(oImg.src, {
+ method: 'GET',
+ })
+ fetch(reqwst)
+ .then((resp) => resp.arrayBuffer())
+ .then(arrayBuffer => {
+ const view = new DataView(arrayBuffer);
+ var oAVM = _obj.findAVMinJPEG(view);
+ _obj.avmdata = true;
+ _obj.tags = oAVM || {};
+ if (typeof fnCallback=="function") fnCallback(_obj);
+ })
+ }
+
+ function addEvent(oElement, strEvent, fncHandler){
+ if (oElement.addEventListener) oElement.addEventListener(strEvent, fncHandler, false);
+ else if (oElement.attachEvent) oElement.attachEvent("on" + strEvent, fncHandler);
+ }
+
+ AVM.prototype.imageHasData = function() {
+ return (this.img.avmdata);
+ }
+
+ AVM.prototype.findAVMinJPEG = function(oFile) {
+ if (oFile.getUint8(0) != 0xFF || oFile.getUint8(1) != 0xD8) return false; // not a valid jpeg
+
+ var iOffset = 2;
+ var iLength = oFile.byteLength;
+ while (iOffset < iLength) {
+ if (oFile.getUint8(iOffset) != 0xFF) return false; // not a valid marker, something is wrong
+ var iMarker = oFile.getUint8(iOffset+1)+1;
+
+
+ // we could implement handling for other markers here,
+ // but we're only looking for 0xFFE1 for AVM data
+ if (iMarker == 22400) {
+ return this.readAVMData(oFile, iOffset + 4, oFile.getUint16(iOffset+2, false)-2);
+ iOffset += 2 + oFile.getUint16(iOffset+2, false);
+
+ } else if (iMarker == 225) {
+ // 0xE1 = Application-specific 1 (for AVM)
+ console.log("jkjk")
+ var oTags = this.readAVMData(oFile, iOffset + 4, oFile.getUint16(iOffset+2, false)-2);
+ return oTags;
+ } else {
+ iOffset += 2 + oFile.getUint16(iOffset+2, false);
+ }
+ }
+ }
+
+ AVM.prototype.readAVMData = function(oFile, iStart, iLength){
+ var oTags = {};
+ this.xmp = this.readXMP(oFile);
+ console.log("xmp read", this.xmp)
+ if (this.xmp) oTags = this.readAVM(this.xmp);
+ return oTags;
+ }
+
+ AVM.prototype.readXMP = function(oFile) {
+ var iEntries = oFile.byteLength;
+ var bBigEnd = false;
+ var prev_n_hex = '';
+ var record = false;
+ var recordn = 0;
+ // Find the XMP packet - 8 bit encoding (UTF-8)
+ // see page 34 of http://www.adobe.com/devnet/xmp/pdfs/xmp_specification.pdf
+ var xmpStr = '0x3C 0x3F 0x78 0x70 0x61 0x63 0x6B 0x65 0x74 0x20 0x62 0x65 0x67 0x69 0x6E 0x3D ';
+ var xmpBytes = 14;
+ var byteStr = '';
+ var iEntryOffset = -1;
+ console.log(iEntryOffset)
+
+ // Here we want to search for the XMP packet starting string
+ // There is probably a more efficient way to search for a byte string
+ for (var i=0;i= 0){
+ var str = '';
+ var i = iEntryOffset;
+ while(str.indexOf('') < 0 && i < (iEntryOffset+20000)){
+ str += String.fromCharCode(oFile.getUint8(i));
+ i++;
+ }
+ return str;
+ }
+ }
+
+ AVM.prototype.readAVM = function(str) {
+ var oTags = {};
+ if(str.indexOf('xmlns:avm') >= 0){
+ for (var keyname in this.AVMdefinedTags) {
+ var key = this.AVMdefinedTags[keyname];
+ key.toLowerCase();
+ var start = str.indexOf(key)+key.length+2;
+ var final = str.indexOf('"',start);
+ // Find out what the character is after the key
+ var char = str.substring(start-2,start-1);
+ if(char == "="){
+ oTags[keyname] = str.substring(start,final);
+ }else if(char == ">"){
+ final = str.indexOf(''+key+'>',start);
+ // Parse out the HTML tags and build an array of the resulting values
+ var tmps = str.substring(start-1,final);
+ console.log(tmps)
+ var tmparr = new Array(0);
+ var tmpstr = tmps.replace(/[\n\r]/g,"");
+ tmpstr = tmpstr.replace(/ +/g," ");
+ tmparr = tmpstr.split(/ ?<\/?[^>]+> ?/g);
+ var newarr = new Array(0);
+ for(var i = 0;i 0) newarr.push(tmparr[i]);
+ }
+ oTags[keyname] = newarr;
+ }
+ }
+ }
+ return oTags;
+ }
+
+ return AVM;
+})();
\ No newline at end of file