Publish Vulpes Preview 15 sources for desktop, Android and Tundra

Share Gaia assets and adapters, keep private profiles and development documents local, and update both public READMEs.
This commit is contained in:
OverSu 2026-09-30 23:13:03 +02:00
commit b38dea0597
25901 changed files with 9982925 additions and 369 deletions

View file

@ -0,0 +1,554 @@
/* exported cropResizeRotate */
/* global getImageSize, LazyLoader, JPEGParser */
/* global Downsample */
//
// Given a blob that represents an encoded image, decode the image, crop it,
// rotate it, resize it, encode it again and pass the encoded blob to the
// callback.
//
// If the image includes EXIF orientation information, it will be
// rotated and/or mirrored so that the proper side is up and EXIF
// orientation information will not be needed in the output blob. The
// blob will not include any EXIF data.
//
// The cropRegion argument is optional. If specfied, it should be an
// object with left, top, width and height properties that specify the
// region of interest in the image. These coordinates should be
// specified as if the image has already been rotated and mirrored. If
// this argument is not specified, then no cropping is done and the
// entire image is returned.
//
// The outputSize argument specifies the desired size of the output
// image. If not specified, then the image is returned full-size. If
// this argument is specified, then it should be an object with width
// and height properties or a single number. If outputSize is an
// object, then the returned image will have the specified size or
// smaller. If the aspect ratio of the output size does not match the
// aspect ratio of the original image or of the crop region, then the
// largest area of the input region that fits the output size without
// letterboxing will be used. If the output size is larger than the
// crop region, then the output size is reduced to match the crop
// region.
//
// If outputSize is a number, then the #-moz-samplesize media fragment
// will be used, if necessary, to ensure that the input image is
// decoded at the specified size or smaller. Note that this media
// fragment gives only coarse control over image size, so passing a
// number for this argument can result in the image being decoded at a
// size substantially smaller than the specified value. If outputSize
// is a number and a crop region is specified, the image may be
// downsampled and then cropped, further reducing the size of the
// resulting image.
//
// The outputType argument specifies the type of the output
// image. Legal values are "image/jpeg", "image/jpeg+exif" and
// "image/png". If not specified, and if the input image does not need
// to be cropped resized or rotated, then it will be returned
// unchanged regardless of the type. If no output type is specified
// and a new blob needs to be created then "image/jpeg" will be
// used. If a type is explicitly specified, and does not match the
// type of the input image, then a new blob will be created even if no
// other changes to the image are necessary. "image/jpeg+exif" is what
// you want if you need to have the Exif in the updated copy.
//
// The optional metadata argument provides a way to pass in image size and
// rotation metadata if you already have it. If this argument is omitted
// or null, getImageSize() will be used to compute the metadata. But if you
// have already called getImageSize() on the blob, you can provide the
// metadata you have and avoid having to reparse the blob.
//
// The callback argument should be a function that expects two arguments.
// If the image is successfully processed, the first argument will be null
// and the second will be a blob. If there was an error, the first argument
// will be an error message and the second argument will be undefined.
//
// If no cropRegion and no outputSize are specified, if the type of the
// input blob matches the requested outputType, and if the image does not
// require any rotation, then this function will not do any work and will
// simply pass the input blob to the callback.
//
// This function requires other shared JS files:
//
// shared/js/blobview.js
// shared/js/media/image_size.js
// shared/js/media/jpeg_metadata_parser.js
// shared/js/media/downsample.js
// shared/js/media/jpeg-exif.js (with LazyLoader)
//
function cropResizeRotate(blob, cropRegion, outputSize, outputType,
metadata, callback)
{
'use strict';
const JPEG = 'image/jpeg';
const PNG = 'image/png';
const EXIF = 'image/jpeg+exif';
// The 2nd, 3rd, 4th and 5th arguments are optional, so fix things up if we're
// called with fewer than 6 args. The last argument is always the callback.
switch (arguments.length) {
case 2:
callback = cropRegion;
cropRegion = outputSize = outputType = metadata = null;
break;
case 3:
callback = outputSize;
outputSize = outputType = metadata = null;
break;
case 4:
callback = outputType;
outputType = metadata = null;
break;
case 5:
callback = metadata;
metadata = null;
break;
case 6:
// everything fine. do nothing here
break;
default:
throw new Error('wrong number of arguments: ' + arguments.length);
}
if (cropRegion) { // make a private copy
cropRegion = {
left: cropRegion.left,
top: cropRegion.top,
width: cropRegion.width,
height: cropRegion.height
};
}
if (outputSize && typeof outputSize === 'object') { // make a private copy
outputSize = {
width: outputSize.width,
height: outputSize.height
};
}
// If we were passed a metadata object, pass it to gotSize. Otherwise,
// find the metadata object first and then pass it.
if (metadata) {
gotSize(metadata);
}
else {
getImageSize(blob, gotSize, function(msg) { callback(msg); });
}
function gotSize(metadata) {
// This is the full size of the image in the input coordiate system
var rawImageWidth = metadata.width;
var rawImageHeight = metadata.height;
var fullsize = rawImageWidth * rawImageHeight;
var rotation = metadata.rotation || 0;
var mirrored = metadata.mirrored || false;
// Compute the full size of the image in the output coordinate system
// I.e. if the image is sideways, swap the width and height
var rotatedImageWidth, rotatedImageHeight;
if (rotation === 0 || rotation === 180) {
rotatedImageWidth = rawImageWidth;
rotatedImageHeight = rawImageHeight;
}
else {
rotatedImageWidth = rawImageHeight;
rotatedImageHeight = rawImageWidth;
}
// If there is no crop region, use the full, rotated image.
// If there is a crop region, make sure it fits inside the image.
if (!cropRegion) {
cropRegion = {
left: 0,
top: 0,
width: rotatedImageWidth,
height: rotatedImageHeight
};
}
else {
if (cropRegion.left < 0 || cropRegion.top < 0 ||
(cropRegion.left + cropRegion.width > rotatedImageWidth) ||
(cropRegion.top + cropRegion.height > rotatedImageHeight)) {
callback('crop region does not fit inside image');
return;
}
}
// If there is no output size, use the size of the crop region.
// If there is an output size make sure it is smaller than the crop region
// and then adjust the crop region as needed so that the aspect ratios
// match
if (outputSize === null || outputSize === undefined) {
outputSize = {
width: cropRegion.width,
height: cropRegion.height
};
}
else if (typeof outputSize === 'number') {
if (outputSize <= 0) {
callback('outputSize must be positive');
return;
}
if (fullsize < outputSize) {
// If the full size of the image is less than the image decode size
// limit, then we can decode the image at full size and use the full
// crop region dimensions as the output size.
outputSize = {
width: cropRegion.width,
height: cropRegion.height
};
}
else {
// In this case we need to specify an output size that is small
// enough that we will be forced below to use #-moz-samplesize
// to downsample the image while decoding it.
// Note that we base this samplesize computation on the full size
// of the image, because we have to decode the entire thing even
// if we are later going to crop it.
var ds = Downsample.areaAtLeast(outputSize / fullsize);
// Now that we've figured out how much the full image will be
// downsampled, scale the crop region to match.
outputSize = {
width: ds.scale(cropRegion.width),
height: ds.scale(cropRegion.height)
};
}
}
if (!(outputSize.width > 0 && outputSize.height > 0)) {
callback('outputSize width and height must be positive');
return;
}
// If the outputSize is bigger than the crop region, just adjust
// the output size to match.
if (outputSize.width > cropRegion.width) {
outputSize.width = cropRegion.width;
}
if (outputSize.height > cropRegion.height) {
outputSize.height = cropRegion.height;
}
// How much do we have to scale the crop region in X and Y dimensions
// to match the output size?
var scaleX = outputSize.width / cropRegion.width;
var scaleY = outputSize.height / cropRegion.height;
// We now adjust the crop region to match the output size. For
// example if the outputSize is 200x200 and the cropRegion is
// 600x400, then scaleX is .33 and scaleY is .5. In this case we can
// leave the height of the crop region alone, but we need to reduce
// the width of the crop region and adjust the left of the crop region
if (scaleY > scaleX) { // adjust width of crop region
var oldCropWidth = cropRegion.width;
cropRegion.width = Math.round(outputSize.width / scaleY);
cropRegion.left += (oldCropWidth - cropRegion.width) >> 1;
}
else if (scaleX > scaleY) { // adjust height of crop region
var oldCropHeight = cropRegion.height;
cropRegion.height = Math.round(outputSize.height / scaleX);
cropRegion.top += (oldCropHeight - cropRegion.height) >> 1;
}
// Make sure the outputType is valid, if one was specified
if (outputType && outputType !== JPEG &&
outputType !== PNG && outputType !== EXIF) {
callback('unsupported outputType: ' + outputType);
return;
}
var preserveExif = false;
if (blob.type === JPEG && outputType === EXIF) {
preserveExif = true;
outputType = JPEG;
}
// Now that we've done these computations, we can pause for a moment
// to see if there is actually any work that needs doing here. If not
// we can just pass the input blob unchanged through to the callback
if (rotation === 0 && // No need to rotate
!mirrored && // or to mirror the image.
(!outputType || // Don't care about output type
blob.type === outputType) && // or type is unchanged.
outputSize.width === rawImageWidth && // Doesn't need crop or resize.
outputSize.height == rawImageHeight) {
callback(null, blob);
return;
}
// The crop region we've been working with so far is in the output
// coordinate system: it assumes that any required rotation has been done.
// In order to know exactly which pixels to extract from the image we
// need to convert to the unrotated, unmirrored input coordinate system.
var inputCropRegion;
// First, handle rotation
switch (rotation) {
case 180:
// The image is upside down. The width and height are the same but
// the top and left have to change.
inputCropRegion = {
left: rawImageWidth - cropRegion.left - cropRegion.width,
top: rawImageHeight - cropRegion.top - cropRegion.height,
width: cropRegion.width,
height: cropRegion.height
};
break;
case 90:
// sideways: swap width and height and adjust top and left
inputCropRegion = {
left: cropRegion.top,
top: rawImageHeight - cropRegion.left - cropRegion.width,
width: cropRegion.height,
height: cropRegion.width
};
break;
case 270:
// sideways: swap width and height and adjust top and left
inputCropRegion = {
left: rawImageWidth - cropRegion.top - cropRegion.height,
top: cropRegion.left,
width: cropRegion.height,
height: cropRegion.width
};
break;
default:
// the crop region is the same in this case
inputCropRegion = {
left: cropRegion.left,
top: cropRegion.top,
width: cropRegion.width,
height: cropRegion.height
};
break;
}
// Next, adjust for mirroring
if (mirrored) {
if (rotation === 90 || rotation === 270) {
inputCropRegion.top =
rawImageHeight - inputCropRegion.top - inputCropRegion.height;
}
else {
inputCropRegion.left =
rawImageWidth - inputCropRegion.left - inputCropRegion.width;
}
}
// In order to decode the image, we create a blob:// URL for it
var baseURL = URL.createObjectURL(blob);
// Decoding an image takes a lot of memory and we want to minimize that.
// Gecko allows us to use media fragments with our image URL to specify
// that we do not want it to decode all of the pixels in the image. The
// #-moz-samplesize= fragment allows us to specify that JPEG images
// should be downsampled while being decoded, and this can save a lot of
// memory.
var sampledsize;
var downsample;
// If we decode the image with a #-moz-samplesize media fragment, both
// the x and y dimensions are reduced by the sample size, so the total
// number of pixels is reduced by the square of the sample size.
if (blob.type === JPEG) {
// What media fragment can we use to downsample the crop region
// so that it is as small as possible without being smaller than
// the output size? We know that the output size and crop
// region have the same aspect ratio now, so we only have to
// consider one dimension. If we passed in a single number outputSize
// up above then we Downsample.areaAtLeast() to compute the outputSize.
// We should now get the same media fragment value here.
downsample =
Downsample.sizeNoMoreThan(outputSize.width / cropRegion.width);
// And if apply that media fragment to the entire image, how big is
// the result?
sampledsize = downsample.scale(rawImageWidth) *
downsample.scale(rawImageHeight);
}
else {
downsample = Downsample.NONE;
sampledsize = fullsize;
}
// Now add the appropriate media fragments to the url
var url;
var resizedWithMediaFragment = false;
if (sampledsize < fullsize) {
// Use a #-moz-samplesize media fragment to downsample while decoding
url = baseURL + downsample;
resizedWithMediaFragment = true;
}
else {
// No media fragments in this case
url = baseURL;
}
// Now we've done our calculations and we have an image URL to decode
var offscreenImage = new Image();
offscreenImage.src = url;
offscreenImage.onerror = function() {
callback('error decoding image: ' + url);
};
offscreenImage.onload = gotImage;
// Called when the image has loaded
function gotImage() {
// If we used a #-moz-samplesize media fragment on the image url,
// and we got an image that is smaller than full-size, then we
// need to reduce the crop region proportionally.
if (resizedWithMediaFragment) {
if (offscreenImage.width < rawImageWidth ||
offscreenImage.height < rawImageHeight) {
var sampleSizeX = rawImageWidth / offscreenImage.width;
var sampleSizeY = rawImageHeight / offscreenImage.height;
inputCropRegion.left =
Math.round(inputCropRegion.left / sampleSizeX);
inputCropRegion.top =
Math.round(inputCropRegion.top / sampleSizeY);
inputCropRegion.width =
Math.round(inputCropRegion.width / sampleSizeX);
inputCropRegion.height =
Math.round(inputCropRegion.height / sampleSizeY);
}
}
// We've decoded the image now, so create a canvas we can copy it into
var canvas = document.createElement('canvas');
var destWidth = canvas.width = outputSize.width;
var destHeight = canvas.height = outputSize.height;
// Since we're only using the canvas as a way to encode the image
// we set this willReadFrequently flag as a hint so that we avoid
// copying the image data to and from the GPU since we don't do any
// GPU operations on it
var context = canvas.getContext('2d', { willReadFrequently: true });
// If the image needs to be rotated or mirrored we have to establish
// an appropriate transform on the context
if (rotation || mirrored) {
// translate so we're rotating around the center
context.translate(canvas.width / 2, canvas.height / 2);
if (mirrored) {
context.scale(-1, 1);
}
// rotate
switch (rotation) {
case 90:
context.rotate(Math.PI / 2);
destWidth = canvas.height;
destHeight = canvas.width;
break;
case 180:
context.rotate(Math.PI);
break;
case 270:
context.rotate(-Math.PI / 2);
destWidth = canvas.height;
destHeight = canvas.width;
break;
}
// And translate back
if (rotation === 90 || rotation === 270) {
// For the 90 and 270 case we swap width and height
context.translate(-canvas.height / 2, -canvas.width / 2);
}
else {
context.translate(-canvas.width / 2, -canvas.height / 2);
}
}
try {
// Now we copy the image into the canvas.
// The image has been loaded, but not decoded yet. If the image file
// appears to be valid and has valid width and height metadata, then
// the onload event handler will fire. But if the image is corrupt
// or too big for gecko to decode with the amount of available
// memory, then this drawImage() call can fail with an exception.
context.drawImage(offscreenImage,
// What part of the image we're drawing
inputCropRegion.left, inputCropRegion.top,
inputCropRegion.width, inputCropRegion.height,
// And what part of the canvas we're drawing it to
0, 0, destWidth, destHeight);
}
catch(e) {
callback('Failed to decode image in cropResizeRotate; ' +
'image may be corrupt or too large: ' + e);
return;
}
finally {
// Once the image has been copied, we can release the decoded image
// memory and the blob URL.
offscreenImage.onload = offscreenImage.onerror = null; offscreenImage.removeAttribute('src');
URL.revokeObjectURL(baseURL);
}
var inputBlob = blob; // XXX just use a different parameter name
// Finally, encode the image into a blob
var postCallback = preserveExif ? copyMetadata : gotEncodedBlob;
canvas.toBlob(postCallback, outputType || JPEG);
function copyMetadata(outputBlob) {
try {
LazyLoader.load(['shared/js/media/jpeg-exif.js'], () => {
JPEGParser.readExifMetaData(inputBlob, (error, metaData) => {
if (error || !metaData) {
gotEncodedBlob(outputBlob);
return;
}
metaData.Orientation = 1;
if (metaData.PixelXDimension) {
metaData.PixelXDimension = outputSize.width;
}
if (metaData.PixelYDimension) {
metaData.PixelYDimension = outputSize.height;
}
JPEGParser.writeExifMetaData(
outputBlob,
metaData,
(error, modifiedBlob) => {
if (error) {
console.error('Error' + error);
}
// Process modified file
gotEncodedBlob(modifiedBlob);
});
});
});
}
catch(e) {
console.log(e.stack);
}
}
function gotEncodedBlob(blob) {
// We have the encoded image but before we pass it to the callback
// we need to free the canvas.
canvas.width = canvas.height = 0;
canvas = context = null;
callback(null, blob);
}
}
}
}

View file

@ -0,0 +1,11 @@
;;(function(exports){'use strict';function round(x){return Math.round(x*100)/100;}
function MozSampleSize(n,scale){return Object.freeze({dimensionScale:round(scale),areaScale:round(scale*scale),toString:function(){return'#-moz-samplesize='+n;},scale:function(x){return Math.ceil(x*scale);}});}
var NONE=Object.freeze({dimensionScale:1,areaScale:1,toString:function(){return'';},scale:function(x){return x;}});var fragments=[NONE,MozSampleSize(2,1/2),MozSampleSize(3,3/8),MozSampleSize(4,1/4),MozSampleSize(8,1/8)];function sizeAtLeast(scale){scale=round(scale);for(var i=0;i<fragments.length;i++){var f=fragments[i];if(f.dimensionScale<=scale){return f;}}
return fragments[fragments.length-1];}
function sizeNoMoreThan(scale){scale=round(scale);for(var i=fragments.length-1;i>=0;i--){var f=fragments[i];if(f.dimensionScale>=scale){return f;}}
return NONE;}
function areaAtLeast(scale){scale=round(scale);for(var i=0;i<fragments.length;i++){var f=fragments[i];if(f.areaScale<=scale){return f;}}
return fragments[fragments.length-1];}
function areaNoMoreThan(scale){scale=round(scale);for(var i=fragments.length-1;i>=0;i--){var f=fragments[i];if(f.areaScale>=scale){return f;}}
return NONE;}
exports.Downsample={sizeAtLeast:sizeAtLeast,sizeNoMoreThan:sizeNoMoreThan,areaAtLeast:areaAtLeast,areaNoMoreThan:areaNoMoreThan,NONE:NONE,MAX_SIZE_REDUCTION:1/fragments[fragments.length-1].dimensionScale,MAX_AREA_REDUCTION:1/fragments[fragments.length-1].areaScale};}(window));

View file

@ -0,0 +1,13 @@
;function getImageSize(blob,callback,error){'use strict';BlobView.get(blob,0,Math.min(1024,blob.size),function(data){if(data.byteLength<=8){error('corrupt image file');return;}
var magic=data.getBinaryText(0,8);if(magic.substring(0,4)==='GIF8'){try{callback({type:'gif',width:data.getUint16(6,true),height:data.getUint16(8,true)});}
catch(e){error(e.toString());}}
else if(magic.substring(0,8)==='\x89PNG\r\n\x1A\n'){try{callback({type:'png',width:data.getUint32(16,false),height:data.getUint32(20,false)});}
catch(e){error(e.toString());}}
else if(magic.substring(0,2)==='BM'&&data.getUint32(2,true)===blob.size){try{var width,height;if(data.getUint16(14,true)===12){width=data.getUint16(18,true);height=data.getUint16(20,true);}
else{width=data.getUint32(18,true);height=data.getUint32(22,true);}
callback({type:'bmp',width:width,height:height});}
catch(e){error(e.toString());}}
else if(magic.substring(0,2)==='\xFF\xD8'){parseJPEGMetadata(blob,function(metadata){if(metadata.progressive){delete metadata.progressive;metadata.type='pjpeg';}
else{metadata.type='jpeg';}
callback(metadata);},error);}
else{error('unknown image type');}});}

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,25 @@
;'use strict';function parseJPEGMetadata(file,metadataCallback,metadataError){var metadata={};var TIFFHeaderOffset=0;BlobView.get(file,0,Math.min(16*1024,file.size),function(data){if(data.byteLength<2||data.getUint8(0)!==0xFF||data.getUint8(1)!==0xD8){metadataError('Not a JPEG file');return;}
getSegment(data,2,segmentHandler);});function getSegment(data,offset,callback){try{var header=data.getUint8(offset);if(header!==0xFF){metadataError('Malformed JPEG file: bad segment header');return;}
var type=data.getUint8(offset+1);var size=data.getUint16(offset+2)+2;var start=data.sliceOffset+data.viewOffset+offset;var isLast=(start+size>=file.size);var length=isLast?size:size+4;data.getMore(start,length,function(data){callback(type,size,data,isLast);});}
catch(e){metadataError(e.toString()+'\n'+e.stack);}}
function segmentHandler(type,size,data,isLastSegment){try{switch(type){case 0xC0:case 0xC1:case 0xC2:case 0xC3:metadata.height=data.getUint16(5);metadata.width=data.getUint16(7);if(type===0xC2){metadata.progressive=true;}
metadataCallback(metadata);break;case 0xE1:parseAPP1(data);default:if(isLastSegment){metadataError('unexpected end of JPEG file');return;}
getSegment(data,size,segmentHandler);}}
catch(e){metadataError(e.toString()+'\n'+e.stack);}}
function parseAPP1(data){if(data.getUint32(4,false)===0x45786966){var exif=parseEXIFData(data);if(exif.THUMBNAIL&&exif.THUMBNAILLENGTH){var start=data.sliceOffset+data.viewOffset+TIFFHeaderOffset+
exif.THUMBNAIL;metadata.preview={start:start,end:start+exif.THUMBNAILLENGTH};}
switch(exif.ORIENTATION){case 2:metadata.rotation=0;metadata.mirrored=true;break;case 3:metadata.rotation=180;metadata.mirrored=false;break;case 4:metadata.rotation=180;metadata.mirrored=true;break;case 5:metadata.rotation=90;metadata.mirrored=true;break;case 6:metadata.rotation=90;metadata.mirrored=false;break;case 7:metadata.rotation=270;metadata.mirrored=true;break;case 8:metadata.rotation=270;metadata.mirrored=false;break;default:metadata.rotation=0;metadata.mirrored=false;break;}}}
function parseEXIFData(data){var exif={};var byteorder=data.getUint8(10);if(byteorder===0x4D){byteorder=false;}else if(byteorder===0x49){byteorder=true;}else{throw Error('invalid byteorder in EXIF segment');}
TIFFHeaderOffset=10;if(data.getUint16(12,byteorder)!==42){throw Error('bad magic number in EXIF segment');}
var offset=data.getUint32(14,byteorder);parseIFD(data,offset+TIFFHeaderOffset,byteorder,exif,true);var ifd0entries=data.getUint16(offset+TIFFHeaderOffset,byteorder);var ifd1=data.getUint32(offset+12+12*ifd0entries,byteorder);if(ifd1!==0){parseIFD(data,ifd1+TIFFHeaderOffset,byteorder,exif,true);}
return exif;}
function parseIFD(data,offset,byteorder,exif,onlyParseOne){var numentries=data.getUint16(offset,byteorder);for(var i=0;i<numentries;i++){parseEntry(data,offset+2+12*i,byteorder,exif);}
if(onlyParseOne){return;}
var next=data.getUint32(offset+2+12*numentries,byteorder);if(next!==0&&next<file.size){parseIFD(data,next+TIFFHeaderOffset,byteorder,exif);}}
var typesize=[0,1,1,2,4,8,1,1,2,4,8,4,8];var tagnames={'274':'ORIENTATION','513':'THUMBNAIL','514':'THUMBNAILLENGTH'};function parseEntry(data,offset,byteorder,exif){var tag=data.getUint16(offset,byteorder);var tagname=tagnames[tag];if(!tagname||exif[tagname]){return;}
var type=data.getUint16(offset+2,byteorder);var count=data.getUint32(offset+4,byteorder);var total=count*typesize[type];var valueOffset=total<=4?offset+8:data.getUint32(offset+8,byteorder)+TIFFHeaderOffset;exif[tagname]=parseValue(data,valueOffset,type,count,byteorder);}
function parseValue(data,offset,type,count,byteorder){var i;if(type===2){var codes=[];for(i=0;i<count-1;i++){codes[i]=data.getUint8(offset+i);}
return String.fromCharCode.apply(String,codes);}else{if(count==1){return parseOneValue(data,offset,type,byteorder);}else{var values=[];var size=typesize[type];for(i=0;i<count;i++){values[i]=parseOneValue(data,offset+size*i,type,byteorder);}
return values;}}}
function parseOneValue(data,offset,type,byteorder){switch(type){case 1:case 7:return data.getUint8(offset);case 2:return null;case 3:return data.getUint16(offset,byteorder);case 4:return data.getUint32(offset,byteorder);case 5:return data.getUint32(offset,byteorder)/data.getUint32(offset+4,byteorder);case 6:return data.getInt8(offset);case 8:return data.getInt16(offset,byteorder);case 9:return data.getInt32(offset,byteorder);case 10:return data.getInt32(offset,byteorder)/data.getInt32(offset+4,byteorder);case 11:return data.getFloat32(offset,byteorder);case 12:return data.getFloat64(offset,byteorder);}
return null;}}

View file

@ -0,0 +1,72 @@
;'use strict';function MediaFrame(container,includeVideo,maxImageSize){this.clear();if(typeof container==='string'){container=document.getElementById(container);}
this.container=container;this.maximumImageSize=maxImageSize||0;this.image=new Image();this.image.className='image-view';this.image.style.opacity=0;this.image.onload=function(){this.style.opacity=1;};this.image.style.transformOrigin='center center';this.image.style.backgroundImage='none';this.image.style.backgroundSize='contain';this.image.style.backgroundRepeat='no-repeat';this.image.style.backgroundColor='#222';this.container.appendChild(this.image);if(includeVideo!==false){this.video=new VideoPlayer(container);this.video.hide();}
container.classList.add('media-frame-container');MediaFrame.instancesToLocalize.set(container,this);}
MediaFrame.instancesToLocalize=new WeakMap();function localize(){for(var container of document.querySelectorAll('.media-frame-container')){var instance=MediaFrame.instancesToLocalize.get(container);if(instance){instance.localize();}}}
document.addEventListener('DOMRetranslated',localize);MediaFrame.computeMaxImageDecodeSize=function(mem){if(!mem){return 0;}
else if(mem<256){return 2*1024*1024;}
else if(mem<512){var screensize=screen.width*window.devicePixelRatio*screen.height*window.devicePixelRatio;if(mem<325&&screensize>480*800){return 2.5*1024*1024;}
return 3*1024*1024;}
else if(mem<1024){return 5*1024*1024;}
else{return(mem/1024)*10*1024*1024;}};if(navigator.getFeature){MediaFrame.pendingPromise=navigator.getFeature('hardware.memory');MediaFrame.pendingPromise.then(function resolve(mem){delete MediaFrame.pendingPromise;MediaFrame.maxImageDecodeSize=MediaFrame.computeMaxImageDecodeSize(mem);},function reject(err){delete MediaFrame.pendingPromise;MediaFrame.maxImageDecodeSize=0;});}
MediaFrame.prototype.displayImage=function displayImage(blob,width,height,preview,rotation,mirrored)
{var self=this;if(MediaFrame.pendingPromise){MediaFrame.pendingPromise.then(function resolve(){self.displayImage(blob,width,height,preview,rotation,mirrored);});return;}
this.clear();this.imageblob=blob;this.fullSampleSize=computeFullSampleSize(blob,width,height);this.fullsizeWidth=this.fullSampleSize.scale(width);this.fullsizeHeight=this.fullSampleSize.scale(height);this.tinyPreviewURL=null;this.imageurl=URL.createObjectURL(blob);this.fullImageURL=this.imageurl+this.fullSampleSize;this.rotation=rotation||0;this.mirrored=mirrored||false;this.displayingImage=true;this.localize();function isValidPreview(preview){if(!preview){return false;}
if(!preview.width||!preview.height){return false;}
if(!preview.start&&!preview.filename){return false;}
if(Math.abs(width/height-preview.width/preview.height)>0.01){return false;}
return true;}
function isBigEnoughPreview(preview){if(!preview){return false;}
if(self.minimumPreviewWidth&&self.minimumPreviewHeight){return Math.max(preview.width,preview.height)>=Math.max(self.minimumPreviewWidth,self.minimumPreviewHeight)&&Math.min(preview.width,preview.height)>=Math.min(self.minimumPreviewWidth,self.minimumPreviewHeight);}
var screenWidth=window.innerWidth*window.devicePixelRatio;var screenHeight=window.innerHeight*window.devicePixelRatio;return((preview.width>=screenWidth||preview.height>=screenHeight)&&(preview.width>=screenHeight||preview.height>=screenWidth));}
var isValid=isValidPreview(preview);var isBigEnough=isBigEnoughPreview(preview);if(isValid&&isBigEnough){if(preview.start){gotPreview(blob.slice(preview.start,preview.end,'image/jpeg'),preview.width,preview.height);}
else{var storage=navigator.getDeviceStorage('pictures');var getreq=storage.get(preview.filename);getreq.onsuccess=function(){gotPreview(getreq.result,preview.width,preview.height);};getreq.onerror=function(){noPreview();};}}
else if(isValid){this.tinyPreviewURL=URL.createObjectURL(blob.slice(preview.start,preview.end,'image/jpeg'));noPreview(this.tinyPreviewURL);}
else{noPreview();}
function gotPreview(previewblob,previewWidth,previewHeight){self.previewurl=URL.createObjectURL(previewblob);self.previewImageURL=self.previewurl;self.previewWidth=previewWidth;self.previewHeight=previewHeight;self.displayingPreview=true;self._displayImage(self.previewImageURL,self.previewWidth,self.previewHeight);}
function noPreview(bgURL){self.previewurl=null;var previewSampleSize=computePreviewSampleSize(blob,width,height);if(previewSampleSize!==Downsample.NONE){self.previewImageURL=self.imageurl+previewSampleSize;self.previewWidth=previewSampleSize.scale(width);self.previewHeight=previewSampleSize.scale(height);self.displayingPreview=true;self._displayImage(self.previewImageURL,self.previewWidth,self.previewHeight,bgURL);}
else{self.previewImageURL=null;self.displayingPreview=false;self._displayImage(self.fullImageURL,self.fullsizeWidth,self.fullsizeHeight);}}
function computeFullSampleSize(blob,width,height){if(blob.type!=='image/jpeg'){return Downsample.NONE;}
var max=MediaFrame.maxImageDecodeSize||0;if(self.maximumImageSize&&(max===0||self.maximumImageSize<max)){max=self.maximumImageSize;}
if(!max||width*height<=max){return Downsample.NONE;}
return Downsample.areaAtLeast(max/(width*height));}
function computePreviewSampleSize(blob,width,height){if(blob.type!=='image/jpeg'){return Downsample.NONE;}
var screenWidth=window.innerWidth*window.devicePixelRatio;var screenHeight=window.innerHeight*window.devicePixelRatio;var portraitScale=Math.min(screenWidth/width,screenHeight/height);var landscapeScale=Math.min(screenHeight/width,screenWidth/height);var scale=Math.max(portraitScale,landscapeScale);return Downsample.sizeNoMoreThan(scale);}};MediaFrame.prototype._displayImage=function(url,width,height,bg){this.image.style.width=width+'px';this.image.style.height=height+'px';if(bg){this.image.style.backgroundImage='url('+bg+')';requestAnimationFrame(()=>{this.image.src=url;});}
else{this.image.style.backgroundImage='none';this.image.src=url;}
if(this.rotation===0||this.rotation===180){this.itemWidth=width;this.itemHeight=height;}else{this.itemWidth=height;this.itemHeight=width;}
this.computeFit();this.setPosition();var temp=this.image.clientLeft;};MediaFrame.prototype.localize=function localize(){if(!this.displayingImage){return;}
var portrait=this.fullsizeWidth<this.fullsizeHeight;if(this.rotation==90||this.rotation==270){portrait=!portrait;}
var timestamp=this.imageblob.lastModifiedDate;var orientationL10nId=portrait?'orientationPortrait':'orientationLandscape';document.l10n.formatValue(orientationL10nId).then((orientationText)=>{if(timestamp){if(!this.dtf){this.dtf=Intl.DateTimeFormat(navigator.languages,{hour12:navigator.mozHour12,hour:'numeric',minute:'numeric',day:'numeric',month:'numeric',year:'numeric'});}
var ts=this.dtf.format(new Date(timestamp));document.l10n.setAttributes(this.image,'imageDescription',{orientation:orientationText,timestamp:ts});}else{document.l10n.setAttributes(this.image,'imageDescriptionNoTimestamp',{orientation:orientationText});}});};MediaFrame.prototype._switchToFullSizeImage=function _switchToFull(){if(!this.displayingImage||!this.displayingPreview){return;}
this.displayingPreview=false;this._displayImage(this.fullImageURL,this.fullsizeWidth,this.fullsizeHeight,this.previewImageURL);};MediaFrame.prototype._switchToPreviewImage=function _switchToPreview(){if(!this.displayingImage||this.displayingPreview||!this.previewImageURL){return;}
this.displayingPreview=true;this._displayImage(this.previewImageURL,this.previewWidth,this.previewHeight);};MediaFrame.prototype.displayVideo=function displayVideo(videoblob,posterblob,aspectRatio,rotation)
{if(!this.video){return;}
this.clear();this.displayingVideo=true;this.videoblob=videoblob;this.posterblob=posterblob;this.videourl=URL.createObjectURL(videoblob);this.posterurl=URL.createObjectURL(posterblob);this.video.load(this.videourl,this.posterurl,aspectRatio,rotation||0,videoblob.lastModifiedDate);this.video.show();};MediaFrame.prototype.clear=function clear(){this.displayingImage=false;this.displayingPreview=false;this.displayingVideo=false;this.itemWidth=this.itemHeight=null;this.imageblob=null;this.videoblob=null;this.posterblob=null;this.fullSampleSize=null;this.fullImageURL=null;this.previewImageURL=null;this.fullsizeWidth=this.fullsizeHeight=null;this.previewWidth=this.previewHeight=null;this.fit=null;if(this.imageurl){URL.revokeObjectURL(this.imageurl);}
this.imageurl=null;if(this.previewurl){URL.revokeObjectURL(this.previewurl);}
this.previewurl=null;if(this.tinyPreviewURL){URL.revokeObjectURL(this.tinyPreviewURL);}
this.tinyPreviewURL=null;if(this.image){this.image.style.opacity=0;this.image.style.backgroundImage='none';this.image.src='';this.image.removeAttribute('aria-label');}
if(this.video){this.video.reset();this.video.hide();if(this.videourl){URL.revokeObjectURL(this.videourl);}
this.videourl=null;if(this.posterurl){URL.revokeObjectURL(this.posterurl);}
this.posterurl=null;}};MediaFrame.prototype.setPosition=function setPosition(){if(!this.fit||!this.displayingImage){return;}
var dx=this.fit.left,dy=this.fit.top;switch(this.rotation){case 0:case 180:dx+=(this.fit.width-this.itemWidth)/2;dy+=(this.fit.height-this.itemHeight)/2;break;case 90:case 270:dx+=(this.fit.width-this.itemHeight)/2;dy+=(this.fit.height-this.itemWidth)/2;break;}
var sx=this.mirrored?-this.fit.scale:this.fit.scale;var sy=this.fit.scale;var transform='translate('+dx+'px, '+dy+'px) '+'scale('+sx+','+sy+')'+'rotate('+this.rotation+'deg) ';this.image.style.transform=transform;};MediaFrame.prototype.computeFit=function computeFit(){if(!this.displayingImage){return;}
this.viewportWidth=this.container.offsetWidth;this.viewportHeight=this.container.offsetHeight;var scalex=this.viewportWidth/this.itemWidth;var scaley=this.viewportHeight/this.itemHeight;var scale=Math.min(Math.min(scalex,scaley),1);var width=Math.floor(this.itemWidth*scale);var height=Math.floor(this.itemHeight*scale);this.fit={width:width,height:height,left:Math.floor((this.viewportWidth-width)/2),top:Math.floor((this.viewportHeight-height)/2),scale:scale,baseScale:scale};};MediaFrame.prototype.reset=function reset(){if(this.displayingImage&&!this.displayingPreview&&this.previewImageURL){this._switchToPreviewImage();return;}
this.computeFit();this.setPosition();if(this.displayingVideo){this.video.setPlayerSize();}};MediaFrame.prototype.resize=function resize(){var oldWidth=this.viewportWidth;var oldHeight=this.viewportHeight;var newWidth=this.container.offsetWidth;var newHeight=this.container.offsetHeight;var oldfit=this.fit;if(!oldfit){return;}
this.computeFit();var newfit=this.fit;if(Math.abs(oldfit.scale-oldfit.baseScale)<0.01||newfit.baseScale>oldfit.scale){this.reset();return;}
oldfit.left+=(newWidth-oldWidth)/2;oldfit.top+=(newHeight-oldHeight)/2;oldfit.baseScale=newfit.baseScale;this.fit=oldfit;this.setPosition();};MediaFrame.prototype.zoom=function zoom(scale,fixedX,fixedY,time){if(!this.displayingImage){return;}
if(this.displayingPreview){this._switchToFullSizeImage();}
if(this.fit.scale*scale>1){scale=1/(this.fit.scale);}
else if(this.fit.scale*scale<this.fit.baseScale){scale=this.fit.baseScale/this.fit.scale;}
this.fit.scale=this.fit.scale*scale;this.fit.width=Math.floor(this.itemWidth*this.fit.scale);this.fit.height=Math.floor(this.itemHeight*this.fit.scale);var photoX=fixedX-this.fit.left;var photoY=fixedY-this.fit.top;photoX=Math.floor(photoX*scale);photoY=Math.floor(photoY*scale);this.fit.left=fixedX-photoX;this.fit.top=fixedY-photoY;if(this.fit.width<=this.viewportWidth){this.fit.left=(this.viewportWidth-this.fit.width)/2;}
else{if(this.fit.left>0){this.fit.left=0;}
if(this.fit.left+this.fit.width<this.viewportWidth){this.fit.left=this.viewportWidth-this.fit.width;}}
if(this.fit.height<=this.viewportHeight){this.fit.top=(this.viewportHeight-this.fit.height)/2;}
else{if(this.fit.top>0){this.fit.top=0;}
if(this.fit.top+this.fit.height<this.viewportHeight){this.fit.top=this.viewportHeight-this.fit.height;}}
if(time){var transition='transform '+time+'ms ease';this.image.style.transition=transition;var self=this;this.image.addEventListener('transitionend',function done(){self.image.removeEventListener('transitionend',done);self.image.style.transition='';});}
this.setPosition();};MediaFrame.prototype.pan=function(dx,dy){if(!this.displayingImage){return dx;}
if(this.fit.height>this.viewportHeight){this.fit.top+=dy;if(this.fit.top>0){this.fit.top=0;}
if(this.fit.top+this.fit.height<this.viewportHeight){this.fit.top=this.viewportHeight-this.fit.height;}}
var extra=0;if(this.fit.width<=this.viewportWidth){extra=dx;}
else{this.fit.left+=dx;if(this.fit.left>0){extra=this.fit.left;this.fit.left=0;}
if(this.fit.left+this.fit.width<this.viewportWidth){extra=this.fit.left+this.fit.width-this.viewportWidth;this.fit.left=this.viewportWidth-this.fit.width;}}
this.setPosition();return extra;};MediaFrame.prototype.setMinimumPreviewSize=function(w,h){this.minimumPreviewWidth=w;this.minimumPreviewHeight=h;};

View file

@ -0,0 +1,11 @@
;'use strict';var MediaUtils={formatDate:function(timestamp){if(!timestamp||timestamp===undefined||isNaN(timestamp)){return;}
return new Date(timestamp).toLocaleString(navigator.languages,{'month':'numeric','year':'numeric','day':'numeric'});},getLocalizedSizeTokens:function(size){if(!size||size===undefined||isNaN(size)){return Promise.resolve('');}
var units=['B','KB','MB','GB','TB','PB','EB','ZB','YB'];var i=0;while(size>=1024&&i<(units.length-1)){size/=1024;++i;}
var sizeDecimal=i<2?Math.round(size):Math.round(size*10)/10;return document.l10n.formatValue('byteUnit-'+units[i]).then((unit)=>{return{size:sizeDecimal,unit};});},getLocalizedSize:function(size){return this.getLocalizedSizeTokens(size).then((args)=>{return document.l10n.formatValue('fileSize',args);});},formatDuration:function(duration){function padLeft(num,length){var r=String(num);while(r.length<length){r='0'+r;}
return r;}
duration=Math.round(duration);var minutes=Math.floor(duration/60);var seconds=duration%60;if(minutes<60){return padLeft(minutes,2)+':'+padLeft(seconds,2);}
var hours=Math.floor(minutes/60);minutes=Math.floor(minutes%60);return hours+':'+padLeft(minutes,2)+':'+padLeft(seconds,2);},populateMediaInfo:function(data){for(var id in data){if(data.hasOwnProperty(id)){var element=document.getElementById(id);if(element){if(typeof data[id]==='string'){element.setAttribute('data-l10n-id',data[id]);}else if(data[id].hasOwnProperty('raw')){element.removeAttribute('data-l10n-id');element.textContent=data[id].raw;}else{document.l10n.setAttributes(element,data[id].id,data[id].args);}}}}},binarySearch:function(array,element,comparator,from,to){if(comparator===undefined){comparator=function(a,b){return a-b;};}
if(from===undefined){return MediaUtils.binarySearch(array,element,comparator,0,array.length);}
if(from===to){return from;}
var mid=Math.floor((from+to)/2);var result=comparator(element,array[mid]);if(result<0){return MediaUtils.binarySearch(array,element,comparator,from,mid);}
else{return MediaUtils.binarySearch(array,element,comparator,mid+1,to);}}};