wip: image format inference, avm parser based on https://www.strudel.org.uk/avm/js/, wcs creation from avm tags

This commit is contained in:
bmatthieu3
2024-07-29 09:45:33 +02:00
committed by Matthieu Baumann
parent 4baab48716
commit c42002db8f
8 changed files with 468 additions and 89 deletions
+1 -1
View File
@@ -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);
});
</script>
+28
View File
@@ -0,0 +1,28 @@
<!doctype html>
<html>
<head>
</head>
<body>
<div id="aladin-lite-div" style="width: 1024px; height: 768px"></div>
<script type="module">
import A from '../src/js/A.js';
A.init.then(() => {
let aladin = A.aladin('#aladin-lite-div', {fov: 30, survey: "CDS/P/PanSTARRS/DR1/color-z-zg-g", target: "280 +0", projection: "AIT", showShareControl:true, showSettingsControl: true, showContextMenu:true});
aladin.setOverlayImageLayer(A.image(
//"https://www.virtualastronomy.org/images/sig05-013.jpg",
"https://www.astropix.org/archive/eso/eso0755a/eso_eso0755a_1280.jpg",
{
name: "sig05-017",
successCallback: (ra, dec, fov, image) => {
console.log(ra, dec)
aladin.gotoRaDec(ra, dec);
aladin.setFoV(fov * 5)
}
},
));
});
</script>
</body>
</html>
-1
View File
@@ -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
+3 -2
View File
@@ -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)?;
+3 -5
View File
@@ -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<js_sys::Promise, JsValue> {
let cfg: ImageMetadata = serde_wasm_bindgen::from_value(cfg)?;
//let wcs: Option<WCSParams> = 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)]
+175 -80
View File
@@ -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);
+2
View File
@@ -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()
}
+256
View File
@@ -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<iEntries;i++) {
var n = oFile.getUint8(i);
var n_hex = n.toString(16).toUpperCase();
if(n_hex.length == 1) n_hex = "0x0"+n_hex;
if(n_hex.length == 2) n_hex = "0x"+n_hex;
if(prev_n_hex == "0x3C" && n_hex == "0x3F"){
record = true;
recordn = xmpBytes;
byteStr = '0x3C ';
}
if(record){
byteStr += n_hex+' ';
console.log(byteStr, recordn)
recordn--;
if(recordn < 0){
if(byteStr == xmpStr){
var iEntryOffset = i-xmpBytes-1;
break;
}
record = false;
}
}
prev_n_hex = n_hex;
}
console.log(iEntryOffset)
if(iEntryOffset >= 0){
var str = '';
var i = iEntryOffset;
while(str.indexOf('</x:xmpmeta>') < 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<tmparr.length;i++){
if(tmparr[i].length > 0) newarr.push(tmparr[i]);
}
oTags[keyname] = newarr;
}
}
}
return oTags;
}
return AVM;
})();