Skip to content

Commit c3ef9c2

Browse files
committed
export jpg
1 parent 761e647 commit c3ef9c2

17 files changed

+387
-57
lines changed

.jshintrc

+1
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@
2929
"DepthReader": false,
3030
"DepthyViewer": false,
3131
"DepthyDrawer": false,
32+
"GDepthEncoder": false,
3233
"Promise": false,
3334
"_": true,
3435
"requestAnimFrame": true,

Gruntfile.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -448,7 +448,7 @@ module.exports = function (grunt) {
448448
karma: {
449449
unit: {
450450
configFile: 'karma.conf.js',
451-
singleRun: true
451+
// singleRun: true
452452
}
453453
}
454454
});

app/index.html

+4
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,9 @@ <h1>Depthy</h1>
6969
<script src="bower_components/is-jpg/browser.js"></script>
7070
<script src="bower_components/is-png/browser.js"></script>
7171
<script src="scripts/vendor/LensBlurDepthExtractor.js"></script>
72+
<script src="scripts/vendor/md5.js"></script>
73+
<script src="scripts/classes/GDepthEncoder.js"></script>
74+
<script src="scripts/classes/dataURITools.js"></script>
7275
<script src="bower_components/promise/index.js"></script>
7376
<script src="bower_components/screenfull/dist/screenfull.js"></script>
7477
<script src="bower_components/iscroll/build/iscroll.js"></script>
@@ -113,6 +116,7 @@ <h1>Depthy</h1>
113116
<script src="scripts/controllers/imageinfomodal.js"></script>
114117
<script src="scripts/controllers/sharepngmodal.js"></script>
115118
<script src="scripts/controllers/exportpngmodal.js"></script>
119+
<script src="scripts/controllers/exportjpgmodal.js"></script>
116120
<script src="scripts/classes/DepthyViewer.js"></script>
117121
<script src="scripts/classes/DepthyDrawer.js"></script>
118122
<script src="scripts/services/statemodal.js"></script>

app/scripts/app.js

+3
Original file line numberDiff line numberDiff line change
@@ -114,6 +114,9 @@ angular.module('depthyApp', [
114114
.state('export.png', {
115115
url: '/png',
116116
})
117+
.state('export.jpg', {
118+
url: '/jpg',
119+
})
117120
.state('export.gif', {
118121
url: '/gif',
119122
})

app/scripts/classes/DepthyViewer.js

+40-16
Original file line numberDiff line numberDiff line change
@@ -880,31 +880,34 @@ Copyright (c) 2014 Rafał Lindemann. http://panrafal.github.com/depthy
880880
};
881881

882882

883-
/** Exports depthmap as is, or converts it to JGP. Returns promise */
884-
this.exportDepthmap = function() {
885-
886-
return this.getPromise().then(
883+
function exportTexture(source, options) {
884+
return source.promise.then(
887885
function() {
888-
if (!hasDepthmap()) {
886+
if (!source.texture) {
889887
return false;
890-
} else if (!depth.useAlpha && depth.url) {
891-
return depth.url;
888+
} else if (options.allowDirect && source.url) {
889+
return source.url;
892890
} else {
891+
var size = sizeCopy(options.size || source.size);
892+
if (options.maxSize) size = sizeFit(size, options.maxSize);
893+
if (options.minSize) size = sizeFit(size, options.minSize, true);
894+
size = sizeRound(size);
895+
893896
var localstage = new PIXI.Stage(),
894-
renderTexture = new PIXI.RenderTexture(depth.size.width, depth.size.height);
897+
scale = sizeFitScale(source.size, size, true),
898+
renderTexture = new PIXI.RenderTexture(size.width, size.height);
895899

896-
var depthSprite = new PIXI.Sprite(depth.texture);
897-
if (depth.useAlpha) {
898-
depthSprite.filters = [invertedAlphaToRGBFilter];
899-
} else {
900-
depthSprite.filters = [grayscaleFilter];
901-
}
900+
var sourceSprite = new PIXI.Sprite(source.texture);
901+
if (options.filters) sourceSprite.filters = options.filters;
902+
sourceSprite.scale = new PIXI.Point(scale, scale);
903+
sourceSprite.anchor = {x: 0.5, y: 0.5};
904+
sourceSprite.position = {x: size.width / 2, y: size.height / 2};
902905

903-
localstage.addChild(depthSprite);
906+
localstage.addChild(sourceSprite);
904907

905908
renderTexture.render(localstage, null, true);
906909
var canvas = PIXI.glReadPixelsToCanvas(renderer.gl, renderTexture, 0, 0, renderTexture.width, renderTexture.height),
907-
dataUrl = canvas.toDataURL('image/jpeg');
910+
dataUrl = canvas.toDataURL(options.type || 'image/jpeg', options.quality || undefined);
908911

909912
try {
910913
renderTexture.destroy();
@@ -915,6 +918,27 @@ Copyright (c) 2014 Rafał Lindemann. http://panrafal.github.com/depthy
915918
}
916919
}
917920
);
921+
}
922+
923+
/** Exports depthmap as is, or converts it to JGP. Returns promise */
924+
this.exportDepthmap = function(options) {
925+
return exportTexture(depth, extend({
926+
// allowDirect: !depth.useAlpha,
927+
filters: depth.useAlpha ? [invertedAlphaToRGBFilter] : [grayscaleFilter],
928+
}, options));
929+
};
930+
931+
this.exportImage = function(options) {
932+
return exportTexture(image, extend({
933+
filters: [resetAlphaFilter]
934+
}, options));
935+
};
936+
937+
this.exportSourceImage = function(source, options) {
938+
source = changeTexture({}, source);
939+
return exportTexture(image, extend({
940+
filters: [resetAlphaFilter]
941+
}, options));
918942
};
919943

920944
this.exportDepthmapAsTexture = function(maxSize) {

app/scripts/classes/GDepthEncoder.js

+173
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,173 @@
1+
/*
2+
The MIT License
3+
4+
Copyright (c) 2014 Rafał Lindemann. http://panrafal.github.com/depthy
5+
*/
6+
(function() {
7+
'use strict';
8+
9+
/*
10+
11+
<x:xmpmeta xmlns:x="adobe:ns:meta/" x:xmptk="Adobe XMP Core 5.1.0-jc003"> <rdf:RDF xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#">
12+
<rdf:Description rdf:about=""
13+
xmlns:GFocus="http://ns.google.com/photos/1.0/focus/"
14+
xmlns:GImage="http://ns.google.com/photos/1.0/image/"
15+
xmlns:GDepth="http://ns.google.com/photos/1.0/depthmap/"
16+
xmlns:xmpNote="http://ns.adobe.com/xmp/note/"
17+
GFocus:BlurAtInfinity="0.02976035"
18+
GFocus:FocalDistance="9.569768"
19+
GFocus:FocalPointX="0.512963" GFocus:FocalPointY="0.49999997"
20+
GImage:Mime="image/jpeg"
21+
GDepth:Format="RangeInverse"
22+
GDepth:Near="6.2360944747924805"
23+
GDepth:Far="19.068166732788086"
24+
GDepth:Mime="image/png"
25+
xmpNote:HasExtendedXMP="420161059863C43993D79FBDFA80C997"/> </rdf:RDF> </x:xmpmeta>
26+
27+
<x:xmpmeta xmlns:x="adobe:ns:meta/" x:xmptk="Adobe XMP Core 5.1.0-jc003">
28+
<rdf:RDF xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#">
29+
<rdf:Description rdf:about="" xmlns:GImage="http://ns.google.com/photos/1.0/image/" xmlns:GDepth="http://ns.google.com/photos/1.0/depthmap/"
30+
GImage:Data="/9j/4AAQSkZJRgABAQAAAQABAAD/2wBDAA...=="
31+
GDepth:Data="iVBORw0KGgoAAAANSUhEUg...C"/>
32+
</rdf:RDF> </x:xmpmeta>
33+
34+
35+
*/
36+
37+
window.GDepthEncoder = {
38+
39+
xmlns: {
40+
'GFocus': 'http://ns.google.com/photos/1.0/focus/',
41+
'GImage': 'http://ns.google.com/photos/1.0/image/',
42+
'GDepth': 'http://ns.google.com/photos/1.0/depthmap/',
43+
'xmpNote': 'http://ns.adobe.com/xmp/note/',
44+
},
45+
46+
// This is NOT a general purpose XMP builder!
47+
buildXMP: function(props, xmlns) {
48+
var xmp = [], k;
49+
xmlns = xmlns || this.xmlns;
50+
xmp.push('<x:xmpmeta xmlns:x="adobe:ns:meta/" x:xmptk="Adobe XMP Core 5.1.0-jc003">');
51+
xmp.push('<rdf:RDF xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#">');
52+
xmp.push('<rdf:Description rdf:about=""');
53+
for (k in xmlns) {
54+
xmp.push(' xmlns:', k, '="', xmlns[k], '"');
55+
}
56+
for (k in props) {
57+
// TODO html entities escaping
58+
xmp.push(' ', k, '="' + props[k] + '"');
59+
}
60+
xmp.push('/></rdf:RDF></x:xmpmeta>');
61+
return xmp.join('');
62+
// xmpNote:HasExtendedXMP="420161059863C43993D79FBDFA80C997"
63+
},
64+
65+
dataURIsplit: function(uri) {
66+
return uri.match(/^data:(.+?);(.+?),(.+)$/);
67+
},
68+
69+
/**
70+
@param ArrayBuffer buffer image JPG as an ArrayBuffer
71+
@param dataURI depthmap
72+
@param dataURI original
73+
*/
74+
encodeDepthmap: function(buffer, depthmap, original, metadata) {
75+
var props = {}, extProps = {}, standardXMP, extendedXMP;
76+
depthmap = this.dataURIsplit(depthmap || '');
77+
original = this.dataURIsplit(original || '');
78+
if (depthmap) {
79+
props['GDepth:Format'] = 'RangeInverse';
80+
props['GDepth:Mime'] = depthmap[1];
81+
extProps['GDepth:Data'] = depthmap[3];
82+
}
83+
if (original) {
84+
props['GImage:Mime'] = original[1];
85+
extProps['GImage:Data'] = depthmap[3];
86+
}
87+
for (var k in metadata || {}) {
88+
props[k] = metadata[k];
89+
}
90+
standardXMP = this.buildXMP(props);
91+
extendedXMP = this.buildXMP(extProps);
92+
93+
return this.encodeXMP(buffer, standardXMP, extendedXMP);
94+
},
95+
96+
encodeXMP: function(buffer, standardXMP, extendedXMP) {
97+
var data = new DataView(buffer),
98+
offset = 0,
99+
parts = [],
100+
xmpWritten = false,
101+
self = this;
102+
103+
function writeXMP() {
104+
if (!xmpWritten) {
105+
parts.push.apply(parts, self.buildXMPsegments(standardXMP, extendedXMP));
106+
console.log('XMP written!');
107+
xmpWritten = true;
108+
}
109+
}
110+
111+
while (offset < data.byteLength) {
112+
var segType, segSize, app1Header, segStart, b;
113+
segStart = offset;
114+
console.log('Offset ' + offset);
115+
if ((b = data.getUint8(offset++)) !== 0xFF) {
116+
throw 'Bad JPG Format, 0xFF expected, got ' + b;
117+
}
118+
do {
119+
segType = data.getUint8(offset++);
120+
if (segType === 0xFF) {
121+
console.log('Padding 0xFF found');
122+
parts.push([0xFF]);
123+
} else break;
124+
} while (true);
125+
if (segType === 0xC0 || segType === 0xC2 || segType === 0xDA) {
126+
writeXMP(); // right before SOF / SOS
127+
}
128+
if (segType === 0xDA) {
129+
// copy the rest on SOS... no XMP should exist beyound that point
130+
console.log('SOS found, copy remaining bytes ' + (buffer.byteLength - segStart));
131+
parts.push(new Uint8Array(buffer, segStart, buffer.byteLength - segStart));
132+
break;
133+
}
134+
if (segType === 0x00 || (segType >= 0xD0 && segType <= 0xD9)) {
135+
parts.push(new Uint8Array(buffer, segStart, 2));
136+
console.log('Found ctrl segment ' + segType);
137+
continue;
138+
}
139+
segSize = data.getUint16(offset);
140+
offset += 2;
141+
if (segType === 0xE1) {
142+
// read header
143+
app1Header = '';
144+
while ((b = data.getUint8(offset++)) !== 0x0) {
145+
app1Header += String.fromCharCode(b);
146+
}
147+
console.log('Found APP1 ' + app1Header);
148+
// ignore any existing XMP
149+
if (app1Header === 'http://ns.adobe.com/xap/1.0/') {
150+
console.log('Found old XMP, skipping');
151+
offset += segSize - (offset - segStart - 2);
152+
continue;
153+
}
154+
}
155+
// copying segment
156+
console.log('Copying segment ' + segType + ', size: ' + segSize + ', left:' + (segSize - (offset - segStart - 2)));
157+
offset += segSize - (offset - segStart - 2);
158+
parts.push(new Uint8Array(buffer, segStart, 1 + segSize));
159+
if (segType === 0xE1) {
160+
writeXMP(); // right after EXIF
161+
}
162+
}
163+
return new Blob(parts, {type: 'image/jpeg'});
164+
},
165+
166+
buildXMPsegments: function(standardXMP, extendedXMP) {
167+
// console.log('StandardXMP: ', standardXMP);
168+
// console.log('ExtendedXMP: ', extendedXMP);
169+
},
170+
171+
};
172+
173+
})();

app/scripts/classes/dataURITools.js

+34
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
(function() {
2+
'use strict';
3+
4+
window.dataURItoArrayBuffer = function dataURItoArrayBuffer(dataURI) {
5+
// convert base64 to raw binary data held in a string
6+
// doesn't handle URLEncoded DataURIs
7+
var byteString;
8+
if (dataURI.split(',')[0].indexOf('base64') >= 0)
9+
byteString = atob(dataURI.split(',')[1]);
10+
else
11+
byteString = window.unescape(dataURI.split(',')[1]);
12+
// separate out the mime component
13+
var mimeString = dataURI.split(',')[0].split(':')[1].split(';')[0];
14+
15+
// write the bytes of the string to an ArrayBuffer
16+
var ab = new ArrayBuffer(byteString.length);
17+
var ia = new Uint8Array(ab);
18+
for (var i = 0; i < byteString.length; i++) {
19+
ia[i] = byteString.charCodeAt(i);
20+
}
21+
22+
return {buffer: ab, mime: mimeString};
23+
};
24+
25+
window.dataURItoBlob = function dataURItoBlob(dataURI) {
26+
var buffer = window.dataURItoArrayBuffer(dataURI);
27+
// write the ArrayBuffer to a blob, and you're done
28+
return new Blob([buffer.buffer],{type: buffer.mime});
29+
};
30+
31+
32+
})();
33+
// credit goes to http://stackoverflow.com/questions/4998908/convert-data-uri-to-file-then-append-to-formdata
34+
+22
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
'use strict';
2+
3+
angular.module('depthyApp')
4+
.controller('ExportJpgModalCtrl', function ($scope, $sce, $timeout, $window, depthy) {
5+
$scope.loading = true;
6+
// wait for animation
7+
$timeout(function() {
8+
depthy.getViewer().exportToPNG(null).then(
9+
function(url) {
10+
// shorten this!
11+
url = URL.createObjectURL(window.dataURItoBlob(url));
12+
var img = angular.element('img[image-source="export-jpg-modal"]')[0];
13+
img.onload = function() {
14+
$scope.loading = false;
15+
$scope.$safeApply();
16+
};
17+
img.src = url;
18+
angular.element('a[image-source="export-jpg-modal"]').attr('href', url);
19+
}
20+
);
21+
}, depthy.modalWait);
22+
});

app/scripts/controllers/exportpngmodal.js

+2-2
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,14 @@
11
'use strict';
22

33
angular.module('depthyApp')
4-
.controller('ExportPngModalCtrl', function ($scope, $sce, $timeout, depthy) {
4+
.controller('ExportPngModalCtrl', function ($scope, $sce, $timeout, $window, depthy) {
55
$scope.loading = true;
66
// wait for animation
77
$timeout(function() {
88
depthy.getViewer().exportToPNG(null).then(
99
function(url) {
1010
// shorten this!
11-
url = URL.createObjectURL(depthy.dataURItoBlob(url));
11+
url = URL.createObjectURL($window.dataURItoBlob(url));
1212
var img = angular.element('img[image-source="export-png-modal"]')[0];
1313
img.onload = function() {
1414
$scope.loading = false;

app/scripts/controllers/main.js

+10
Original file line numberDiff line numberDiff line change
@@ -112,6 +112,16 @@ angular.module('depthyApp')
112112
});
113113
};
114114

115+
$scope.exportJpgRun = function() {
116+
StateModal.showModal('export.jpg', {
117+
// stateOptions: {location: 'replace'},
118+
templateUrl: 'views/export-jpg-modal.html',
119+
controller: 'ExportJpgModalCtrl',
120+
windowClass: 'export-jpg-modal',
121+
}).result.finally(function() {
122+
});
123+
};
124+
115125
$scope.sharePngRun = function() {
116126
StateModal.showModal('share.png', {
117127
// stateOptions: {location: 'replace'},

0 commit comments

Comments
 (0)