Compare commits

...

4 Commits
develop ... avm

Author SHA1 Message Date
Matthieu Baumann
3d060c12e9 remove tiff dep 2024-07-29 09:29:30 +02:00
bmatthieu3
dd55f8904b wip tiff support 2024-07-26 19:07:41 +02:00
Matthieu Baumann
2c0afd8b84 wip parse the header inside xmp 2024-07-26 09:41:55 +02:00
bmatthieu3
245030fb0a wip: image format inference, avm parser based on https://www.strudel.org.uk/avm/js/, wcs creation from avm tags 2024-07-24 17:52:57 +02:00
11 changed files with 583 additions and 115 deletions

View File

@@ -14,7 +14,7 @@
A.init.then(() => { A.init.then(() => {
aladin = A.aladin('#aladin-lite-div', {target: "05 40 59.12 -02 27 04.1", fov: 2}); 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); aladin.setBaseImageLayer(survey);
}); });
</script> </script>

View File

@@ -0,0 +1,27 @@
<!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/DSS2/color", target: "280 +0", projection: "AIT", showShareControl:true, showSettingsControl: true, showContextMenu:true});
aladin.setOverlayImageLayer(A.image(
"https://www.virtualastronomy.org/images/sig05-013.jpg",
{
name: "sig05-017",
successCallback: (ra, dec, fov, image) => {
console.log(ra, dec)
aladin.gotoRaDec(ra, dec);
aladin.setFoV(fov * 5)
}
},
));
});
</script>
</body>
</html>

View File

@@ -13,7 +13,6 @@
"https://nova.astrometry.net/image/25038473?filename=M61.jpg", "https://nova.astrometry.net/image/25038473?filename=M61.jpg",
{ {
name: "M61", name: "M61",
imgFormat: 'jpeg',
wcs: { wcs: {
NAXIS: 0, // Minimal header NAXIS: 0, // Minimal header
CTYPE1: 'RA---TAN', // TAN (gnomic) projection CTYPE1: 'RA---TAN', // TAN (gnomic) projection
@@ -28,9 +27,9 @@
CUNIT1: 'deg', // X pixel scale units CUNIT1: 'deg', // X pixel scale units
CUNIT2: 'deg', // Y pixel scale units CUNIT2: 'deg', // Y pixel scale units
CD1_1: -0.000223666022989, // Transformation matrix CD1_1: -0.000223666022989, // Transformation matrix
CD1_2: 0.000296578064584, // no comment CD1_2: -0.000296578064584, // no comment
CD2_1: -0.000296427555509, // no comment CD2_1: -0.000296427555509, // no comment
CD2_2: -0.000223774308964, // no comment CD2_2: 0.000223774308964, // no comment
NAXIS1: 1080, // Image width, in pixels. NAXIS1: 1080, // Image width, in pixels.
NAXIS2: 705 // Image height, in pixels. NAXIS2: 705 // Image height, in pixels.
}, },

View File

@@ -90,7 +90,6 @@ pub struct App {
_fbo_view: FrameBufferObject, _fbo_view: FrameBufferObject,
_fbo_ui: FrameBufferObject, _fbo_ui: FrameBufferObject,
//line_renderer: RasterizedLineRenderer, //line_renderer: RasterizedLineRenderer,
colormaps: Colormaps, colormaps: Colormaps,
pub projection: ProjectionType, pub projection: ProjectionType,
@@ -1115,7 +1114,6 @@ impl App {
pub(crate) fn add_image_fits( pub(crate) fn add_image_fits(
&mut self, &mut self,
id: String,
stream: web_sys::ReadableStream, stream: web_sys::ReadableStream,
meta: ImageMetadata, meta: ImageMetadata,
layer: String, layer: String,
@@ -1245,8 +1243,9 @@ impl App {
} else { } else {
let fits = ImageLayer { let fits = ImageLayer {
images, images,
id: layer.clone(),
layer, layer,
id,
meta, meta,
}; };
@@ -1301,6 +1300,7 @@ impl App {
) -> Result<(), JsValue> { ) -> Result<(), JsValue> {
let old_meta = self.layers.get_layer_cfg(&layer)?; let old_meta = self.layers.get_layer_cfg(&layer)?;
// Set the new meta // Set the new meta
// keep the old meta data
let new_img_fmt = meta.img_format; let new_img_fmt = meta.img_format;
self.layers self.layers
.set_layer_cfg(layer.clone(), meta, &mut self.camera, &self.projection)?; .set_layer_cfg(layer.clone(), meta, &mut self.camera, &self.projection)?;

View File

@@ -55,8 +55,10 @@ pub fn build_fov_coverage(
moc moc
} }
} else { } else {
let center_xyzw = crate::coosys::apply_coo_system(camera_frame, frame, camera_center);
let biggest_fov_rad = proj.aperture_start().to_radians(); let biggest_fov_rad = proj.aperture_start().to_radians();
let lonlat = camera_center.lonlat(); let lonlat = center_xyzw.lonlat();
HEALPixCoverage::from_cone(&lonlat, biggest_fov_rad * 0.5, depth) HEALPixCoverage::from_cone(&lonlat, biggest_fov_rad * 0.5, depth)
} }
} }

View File

@@ -382,21 +382,19 @@ impl WebClient {
#[wasm_bindgen(js_name = addImageFITS)] #[wasm_bindgen(js_name = addImageFITS)]
pub fn add_image_fits( pub fn add_image_fits(
&mut self, &mut self,
id: String,
stream: web_sys::ReadableStream, stream: web_sys::ReadableStream,
cfg: JsValue, cfg: JsValue,
layer: String, layer: String,
) -> Result<js_sys::Promise, JsValue> { ) -> Result<js_sys::Promise, JsValue> {
let cfg: ImageMetadata = serde_wasm_bindgen::from_value(cfg)?; 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)] #[wasm_bindgen(js_name = addImageWithWCS)]
pub fn add_image_with_wcs( pub fn add_image_with_wcs(
&mut self, &mut self,
data: web_sys::ReadableStream, stream: web_sys::ReadableStream,
wcs: JsValue, wcs: JsValue,
cfg: JsValue, cfg: JsValue,
layer: String, layer: String,
@@ -406,7 +404,7 @@ impl WebClient {
let wcs_params: WCSParams = serde_wasm_bindgen::from_value(wcs)?; let wcs_params: WCSParams = serde_wasm_bindgen::from_value(wcs)?;
let wcs = WCS::new(&wcs_params).map_err(|e| JsValue::from_str(&format!("{:?}", e)))?; 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)] #[wasm_bindgen(js_name = removeLayer)]

View File

@@ -11,6 +11,6 @@ uniform float opacity;
#include ../hips/color.glsl; #include ../hips/color.glsl;
void main() { void main() {
out_frag_color = texture(tex, frag_uv); out_frag_color = texture(tex, vec2(frag_uv.x, 1.0 - frag_uv.y));
out_frag_color.a = out_frag_color.a * opacity; out_frag_color.a = out_frag_color.a * opacity;
} }

View File

@@ -6,7 +6,7 @@ vec2 w2c_ait(vec3 p) {
float y2d = p.y / w; float y2d = p.y / w;
float x2d = 0.0; float x2d = 0.0;
if (abs(p.x) < 1e-3) { if (abs(p.x) < 5e-3) {
float x_over_r = p.x/r; float x_over_r = p.x/r;
x2d = -p.x * (1.0 - x_over_r*x_over_r/21.0) / w; x2d = -p.x * (1.0 - x_over_r*x_over_r/21.0) / w;
} else { } else {

View File

@@ -28,6 +28,8 @@
import { ALEvent } from "./events/ALEvent.js"; import { ALEvent } from "./events/ALEvent.js";
import { ColorCfg } from "./ColorCfg.js"; import { ColorCfg } from "./ColorCfg.js";
import { Aladin } from "./Aladin.js"; import { Aladin } from "./Aladin.js";
import { Utils } from "./Utils";
import { AVM } from "./libs/avm.js";
/** /**
* @typedef {Object} ImageOptions * @typedef {Object} ImageOptions
@@ -48,7 +50,7 @@ import { Aladin } from "./Aladin.js";
* @property {number} [contrast=0.0] - The contrast value for the color configuration. * @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 * @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. * 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'. tiff files are not supported. You can convert your tiff files to jpg ones by using the fantastic image magick suite.
* *
* @example * @example
* *
@@ -56,26 +58,25 @@ import { Aladin } from "./Aladin.js";
* "https://nova.astrometry.net/image/25038473?filename=M61.jpg", * "https://nova.astrometry.net/image/25038473?filename=M61.jpg",
* { * {
* name: "M61", * name: "M61",
* imgFormat: 'jpeg',
* wcs: { * wcs: {
* NAXIS: 0, // Minimal header NAXIS: 0, // Minimal header
* CTYPE1: 'RA---TAN', // TAN (gnomic) projection + SIP distortions CTYPE1: 'RA---TAN', // TAN (gnomic) projection
* CTYPE2: 'DEC--TAN', // TAN (gnomic) projection + SIP distortions CTYPE2: 'DEC--TAN', // TAN (gnomic) projection
* EQUINOX: 2000.0, // Equatorial coordinates definition (yr) EQUINOX: 2000.0, // Equatorial coordinates definition (yr)
* LONPOLE: 180.0, // no comment LONPOLE: 180.0, // no comment
* LATPOLE: 0.0, // no comment LATPOLE: 0.0, // no comment
* CRVAL1: 185.445488837, // RA of reference point CRVAL1: 185.445488837, // RA of reference point
* CRVAL2: 4.47896032431, // DEC of reference point CRVAL2: 4.47896032431, // DEC of reference point
* CRPIX1: 588.995094299, // X reference pixel CRPIX1: 588.995094299, // X reference pixel
* CRPIX2: 308.307905197, // Y reference pixel CRPIX2: 308.307905197, // Y reference pixel
* CUNIT1: 'deg', // X pixel scale units CUNIT1: 'deg', // X pixel scale units
* CUNIT2: 'deg', // Y pixel scale units CUNIT2: 'deg', // Y pixel scale units
* CD1_1: -0.000223666022989, // Transformation matrix CD1_1: -0.000223666022989, // Transformation matrix
* CD1_2: 0.000296578064584, // no comment CD1_2: -0.000296578064584, // no comment
* CD2_1: -0.000296427555509, // no comment CD2_1: -0.000296427555509, // no comment
* CD2_2: -0.000223774308964, // no comment CD2_2: 0.000223774308964, // no comment
* NAXIS1: 1080, // Image width, in pixels. NAXIS1: 1080, // Image width, in pixels.
* NAXIS2: 705 // Image height, in pixels. NAXIS2: 705 // Image height, in pixels.
* }, * },
* successCallback: (ra, dec, fov, image) => { * successCallback: (ra, dec, fov, image) => {
* aladin.gotoRaDec(ra, dec); * aladin.gotoRaDec(ra, dec);
@@ -104,8 +105,8 @@ export let Image = (function () {
this.url = url; this.url = url;
this.id = url; this.id = url;
this.name = (options && options.name) || this.url; this.name = (options && options.name) || this.url;
this.imgFormat = (options && options.imgFormat) || "fits"; this.imgFormat = options && options.imgFormat;
this.formats = [this.imgFormat]; //this.formats = [this.imgFormat];
// callbacks // callbacks
this.successCallback = options && options.successCallback; this.successCallback = options && options.successCallback;
this.errorCallback = options && options.errorCallback; this.errorCallback = options && options.errorCallback;
@@ -116,7 +117,7 @@ export let Image = (function () {
}*/ }*/
this.colorCfg = new ColorCfg(options); this.colorCfg = new ColorCfg(options);
this.options = options; this.options = options || {};
let self = this; let self = this;
@@ -219,92 +220,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
)
},
error: (e) => {
// try as cors
const url = Aladin.JSONP_PROXY + '?url=' + self.url;
return Utils.fetch({
url: 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);
resolve(stream)
};
if (!self.options.wcs) {
/* look for avm tags if no wcs is given */
let avm = new AVM(img);
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.wcsdata (Boolean) = If WCS have been loaded
// obj.tags (object) = An array containing all the loaded tags e.g. obj.tags['Headline']
// obj.wcs (object) = The wcs parsed from the image
if (obj.wcsdata) {
if (img.width !== obj.wcs.NAXIS1) {
obj.wcs.NAXIS1 = img.width;
}
if (img.height !== obj.wcs.NAXIS2) {
obj.wcs.NAXIS2 = img.height;
}
self.options.wcs = obj.wcs;
img2Blob()
} else {
// no tags found
reject('No WCS have been found in the image')
return;
}
})
} else {
img2Blob()
}
}
let proxyUsed = false;
img.onerror = (e) => {
// use proxy
if (proxyUsed) {
console.error(e);
reject('Error parsing image located at: ' + 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) { Image.prototype.add = function (layer) {
this.layer = layer; this.layer = layer;
let self = this; let self = this;
let promise; let promise;
if (this.imgFormat === 'fits') { if (this.imgFormat === 'fits') {
let id = this.url; promise = this._addFITS(layer)
promise = fetch(this.url) .catch(e => {
.then((resp) => resp.body) console.error(`Image located at ${this.url} could not be parsed as fits file. Is the imgFormat specified correct?`)
.then((readableStream) => { return Promise.reject(e)
return self.view.wasm
.addImageFITS(
id,
readableStream,
{
...self.colorCfg.get(),
longitudeReversed: false,
imgFormat: self.imgFormat,
},
layer
)
}) })
} else if (this.imgFormat === 'jpg' || this.imgFormat === 'jpeg') { } else if (this.imgFormat === 'jpeg' || this.imgFormat === 'png') {
let img = document.createElement('img'); promise = this._addJPGOrPNG(layer)
.catch(e => {
promise = console.error(`Image located at ${this.url} could not be parsed as a ${this.imgFormat} file. Is the imgFormat specified correct?`);
new Promise((resolve, reject) => { return Promise.reject(e)
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 { } else {
console.warn(`Image format: ${this.imgFormat} not supported`); // imgformat not defined we will try first supposing it is a fits file and then use the jpg heuristic
promise = Promise.reject(); promise = self._addFITS(layer)
}; .catch(e => {
return self._addJPGOrPNG(layer)
.catch(e => {
console.error(`Image located at ${self.url} could not be parsed as jpg/png/tif image file. Aborting...`)
return Promise.reject(e);
})
})
}
promise = promise.then((imageParams) => { promise = promise.then((imageParams) => {
self.formats = [self.imgFormat];
// There is at least one entry in imageParams // There is at least one entry in imageParams
self.added = true; self.added = true;
self.setView(self.view); self.setView(self.view);

View File

@@ -378,9 +378,11 @@ Utils.fetch = function(params) {
return fetch(request) return fetch(request)
.then((resp) => { .then((resp) => {
if (params.dataType && params.dataType.includes('json')) { if (params.dataType && params.dataType.includes('json')) {
return resp.json() return resp.json();
} else if (params.dataType && params.dataType.includes('blob')) { } else if (params.dataType && params.dataType.includes('blob')) {
return resp.blob() return resp.blob();
} else if (params.dataType && params.dataType.includes('readableStream')) {
return Promise.resolve(resp.body);
} else { } else {
return resp.text() return resp.text()
} }

346
src/js/libs/avm.js Normal file
View File

@@ -0,0 +1,346 @@
/*
* 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
*
* S
*/
export let AVM = (function() {
function AVM(input) {
if (input instanceof HTMLImageElement) {
this.img = input;
} else if (input instanceof ArrayBuffer) {
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.wcsdata = false;
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 instanceof ArrayBuffer) {
this.getData(fnCallback);
return;
}
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;
const findAVM = (arrayBuffer) => {
const view = new DataView(arrayBuffer);
var oAVM = _obj.findAVMinJPEG(view);
_obj.wcs = oAVM || {};
_obj.wcsdata = _obj.wcs !== undefined && Object.keys(_obj.wcs).length > 0;
if (typeof fnCallback=="function") fnCallback(_obj);
};
if (oImg instanceof ArrayBuffer) {
findAVM(oImg)
} else {
let reqwst = new Request(oImg.src, {
method: 'GET',
})
fetch(reqwst)
.then((resp) => resp.arrayBuffer())
.then(arrayBuffer => {
findAVM(arrayBuffer)
})
}
}
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.wcsdata);
}
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);
// we could implement handling for other markers here,
// but we're only looking for 0xFFE1 for AVM data
if (iMarker == 22400) {
return this.readAVMDataAsWCS(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)
return this.readAVMDataAsWCS(oFile, iOffset + 4, oFile.getUint16(iOffset+2, false)-2);
} else {
iOffset += 2 + oFile.getUint16(iOffset+2, false);
}
}
}
AVM.prototype.readAVMDataAsWCS = function(oFile) {
var tags = undefined;
let wcs = {};
this.xmp = this.readXMP(oFile);
if (this.xmp) {
tags = this.readAVM(this.xmp);
if (tags) {
this.tags = tags;
let unwindTag = (tag) => {
if (Array.isArray(tag)) {
return tag[0]
} else {
return tag;
}
}
wcs.CTYPE1 = unwindTag(tags['Spatial.CoordinateFrame']) === 'ICRS' ? 'RA---' : 'GLON-';
wcs.CTYPE1 += unwindTag(tags['Spatial.CoordsystemProjection']);
wcs.CTYPE2 = unwindTag(tags['Spatial.CoordinateFrame']) === 'ICRS' ? 'DEC--' : 'GLAT-';
wcs.CTYPE2 += unwindTag(tags['Spatial.CoordsystemProjection']);
if (unwindTag(tags['Spatial.Equinox']))
wcs.EQUINOX = +unwindTag(tags['Spatial.Equinox']);
wcs.NAXIS1 = tags['Spatial.ReferenceDimension'] && +tags['Spatial.ReferenceDimension'][0];
wcs.NAXIS2 = tags['Spatial.ReferenceDimension'] && +tags['Spatial.ReferenceDimension'][1];
if (tags['Spatial.CDMatrix']) {
console.warn("Spatial.CDMatrix is deprecated in favor of Spatial.Scale + Spatial.Rotation");
wcs.CD1_1 = +tags['Spatial.CDMatrix'][0];
wcs.CD1_2 = +tags['Spatial.CDMatrix'][1];
wcs.CD2_1 = +tags['Spatial.CDMatrix'][2];
wcs.CD2_2 = +tags['Spatial.CDMatrix'][3];
} else {
wcs.CDELT1 = tags['Spatial.Scale'] && +tags['Spatial.Scale'][0];
wcs.CDELT2 = tags['Spatial.Scale'] && +tags['Spatial.Scale'][1];
if (unwindTag(tags['Spatial.Rotation']) !== undefined) {
wcs.CROTA2 = +unwindTag(tags['Spatial.Rotation']);
}
}
wcs.CRPIX1 = tags['Spatial.ReferencePixel'] && +tags['Spatial.ReferencePixel'][0];
wcs.CRPIX2 = tags['Spatial.ReferencePixel'] && +tags['Spatial.ReferencePixel'][1];
wcs.CRVAL1 = tags['Spatial.ReferenceValue'] && +tags['Spatial.ReferenceValue'][0];
wcs.CRVAL2 = tags['Spatial.ReferenceValue'] && +tags['Spatial.ReferenceValue'][1];
} else {
var equalReached = false;
for(var key of ['NAXIS1', 'NAXIS2', 'CTYPE1', 'CTYPE2', 'CRPIX1', 'CRPIX2', 'CRVAL1', 'CRVAL2', 'LONPOLE', 'LATPOLE', 'CDELT1', 'CDELT2', 'PC1_1', 'PC2_2', 'PC1_2', 'PC2_1', 'CD1_1', 'CD2_2', 'CD1_2', 'CD2_1']) {
equalReached = false;
// try to read directly the WCS
let beginCard = this.xmp.slice(this.xmp.indexOf(key));
let values = beginCard.split(" ");
for (var v of values) {
if (equalReached && v !== "") {
wcs[key] = parseFloat(v);
if (Number.isNaN(wcs[key])) {
if (v[0] === "'" || v[0] === "\"") {
v = v.slice(1, v.length - 1)
}
wcs[key] = v;
}
break;
}
if (v === "=") {
equalReached = true;
}
}
}
}
}
return wcs;
}
AVM.prototype.readXMP = function(oFile) {
var iEntries = oFile.byteLength;
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;
// 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+' ';
recordn--;
if(recordn < 0){
if(byteStr == xmpStr){
var iEntryOffset = i-xmpBytes-1;
break;
}
record = false;
}
}
prev_n_hex = n_hex;
}
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 tags = undefined;
if(str.indexOf('xmlns:avm') >= 0){
tags = {}
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 == "="){
tags[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);
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]);
}
tags[keyname] = newarr;
}
}
}
return tags;
}
return AVM;
})();