├── .gitignore
├── README.md
├── app
├── ImageOpener.js
├── capture-image.html
├── capture-image.js
├── cssgram.min.css
├── index.html
├── index.js
├── lightbroom.css
├── lightbroom.js
├── package.json
├── photo.js
├── svg-image.js
├── transfer-image.js
└── vendor
│ └── exif.js
└── package.json
/.gitignore:
--------------------------------------------------------------------------------
1 |
2 | node_modules/*
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | Lightbroom
2 | --
3 |
4 | Back in 2003, I created [this repository](https://github.com/zz85/lightbroom/commit/124270abe79588366e7bd041f32851aa548ddbc9) because I had some [wild ideas](https://plus.google.com/117614030945250277079/posts/LcApD7CFF86) of building a better replacement to my lightroom workflow... (yeah it is never that easy but one can always dream...)
5 |
6 | ## Current Features
7 |
8 | - Drag drop photos
9 | - Mass preview and apply filters
10 | - Mass export
11 |
12 | ## Running the app
13 |
14 | `electron app.js`
15 |
16 | ### Prerequisite
17 |
18 | Electron
19 |
20 | `npm install -g electron-prebuilt`
21 |
22 | Node modules
23 |
24 | `npm install --production`
25 |
26 | ## Current State
27 |
28 | An Instagram-like desktop app developed with Electron and Web technologies for a simple photo retouching workflow.
29 | This iteration uses [CSSgram](https://github.com/una/CSSgram/) (that uses GPU accelerated CSS filters and blend modes) for Instagram like effects.
30 |
31 | 
32 |
33 | 
34 |
35 | ## Contact
36 | [@blurspline](http://twitter.com/blurspline)
37 |
--------------------------------------------------------------------------------
/app/ImageOpener.js:
--------------------------------------------------------------------------------
1 | //
2 | // this is a simple image opener util file that supports
3 | // image loading via
4 | // 1. drag and drop
5 | // 2. browsing
6 | // 3. TODO clipboard paste
7 | // Load 3. Integration as JS file for demo
8 |
9 | function ImageOpener( processImage, target ) {
10 |
11 | var debug = true;
12 | // Check for the various File API support.
13 | if (window.File && window.FileReader && window.FileList && window.Blob) {
14 | // Great success! All the File APIs are supported.
15 | window.URL = window.URL || window.webkitURL;
16 | } else {
17 | return console.error('The File APIs are not fully supported in this browser.');
18 | }
19 |
20 | target = target || document.body;
21 |
22 | // Input Field
23 | var input = document.createElement('input');
24 | input.type = 'file';
25 | input.addEventListener('change', handleFileSelect, false);
26 | this.input = input;
27 |
28 | // target.appendChild(input);
29 |
30 | target.onpaste = handlepaste;
31 |
32 | /***************************************
33 | Event handlers for file load
34 | ****************************************/
35 | function handleFileSelect(evt) {
36 | var files = evt.target.files; // FileList object
37 |
38 | // files is a FileList of File objects. List some properties.
39 | for (var i = 0, f; f = files[i]; i++) {
40 | if (debug) {
41 | console.log( 'detected', f.name, f.type,
42 | f.size, ' bytes, last modified: ',
43 | f.lastModifiedDate ? f.lastModifiedDate.toLocaleDateString() : 'n/a');
44 | }
45 |
46 | openFile(files[i], i);
47 | }
48 | }
49 |
50 | function handlepaste (e) {
51 | if (e && e.clipboardData && e.clipboardData.getData) {
52 | // Webkit - get data from clipboard, put into editdiv, cleanup, then cancel event
53 | // ii = e.clipboardData.getData('jpg/image');
54 | var items = e.clipboardData.items;
55 |
56 | var i = 0;
57 | if (items[i].kind == 'file' &&
58 | items[i].type.indexOf('image/') !== -1) {
59 | var blob = items[i].getAsFile();
60 | var blobUrl = URL.createObjectURL(blob);
61 |
62 | var img = document.createElement('img');
63 | img.src = blobUrl;
64 | img.onload = function(e) {
65 | processImage(img);
66 | };
67 | }
68 |
69 | // console.log(e, e.clipboardData.getData('text/plain'));
70 | if (e.preventDefault) {
71 | e.stopPropagation();
72 | e.preventDefault();
73 | }
74 | return false;
75 | }
76 | }
77 | /***************************************
78 | Event handlers for drag and drop
79 | ****************************************/
80 | target.addEventListener('dragover', handleDragOver, false);
81 | target.addEventListener('dragenter', stopDefault, false);
82 | target.addEventListener('dragexit', stopDefault, false);
83 |
84 | target.addEventListener('drop', dropBehavior, false);
85 |
86 | /***************************************
87 | Event Callbacks for drag and drop
88 | ****************************************/
89 |
90 |
91 | function handleDragOver(e) {
92 | if (e.dataTransfer.files) {
93 | e.stopPropagation();
94 | e.preventDefault();
95 | e.dataTransfer.dropEffect = 'copy'; // Explicitly show this is a copy.
96 | }
97 | }
98 |
99 |
100 | function stopDefault(e) {
101 | e.preventDefault();
102 | return false;
103 | }
104 |
105 | function dropBehavior(e) {
106 | var files = e.dataTransfer.files;
107 |
108 | if (files) {
109 | e.stopPropagation();
110 | e.preventDefault();
111 | } else {
112 | console.log('Files are dropped', e);
113 | return;
114 | }
115 |
116 | if (files.length) {
117 | for (var i = 0; i < files.length; i++) {
118 | openFile(files[i], i);
119 | }
120 | } else {
121 | // TODO support copypaste/clipboard drag in
122 | }
123 | }
124 |
125 | function openFile(file, i) {
126 | if (!file.type.match('image.*')) {
127 | if (debug) console.log('image fail');
128 | return;
129 | }
130 |
131 | var reader = new FileReader();
132 | reader.onloadend = function(e) {
133 | loadImage(e, file.path, i);
134 | };
135 |
136 | reader.onerror = errorHandler;
137 |
138 | reader.onprogress = updateProgress;
139 | reader.onabort = function(e) {
140 | console.log('File read cancelled');
141 | };
142 |
143 | reader.onloadstart = function(e) {
144 | console.log('onloadstart');
145 | };
146 |
147 | reader.onload = function(e) {
148 | console.log('onload');
149 | };
150 |
151 | reader.readAsArrayBuffer(file);
152 | // reader.readAsBinaryString(file);
153 | // reader.readAsDataURL(file);
154 | }
155 |
156 | function loadImage(e, path, i) {
157 | var arraybuffer = e.target.result;
158 | // console.log('loadImage', e); // e, target, reader, e.target.result
159 |
160 | var blob = new Blob([arraybuffer]);
161 | var objectURL = URL.createObjectURL(blob);
162 |
163 | var img = document.createElement("img");
164 | img.src = objectURL;
165 |
166 | img.onload = function(e) {
167 | processImage(img, path, i);
168 | };
169 | }
170 |
171 |
172 | function updateProgress(e) {
173 | // evt is an ProgressEvent.
174 | if (e.lengthComputable) {
175 | var percentLoaded = Math.round((e.loaded / e.total) * 100);
176 |
177 | if (percentLoaded < 100) {
178 | console.log(percentLoaded + '%');
179 | }
180 | }
181 | }
182 |
183 |
184 | function errorHandler(e) {
185 | switch (e.target.error.code) {
186 | case e.target.error.NOT_FOUND_ERR:
187 | alert('File Not Found!');
188 | break;
189 | case e.target.error.NOT_READABLE_ERR:
190 | alert('File is not readable');
191 | break;
192 | case e.target.error.ABORT_ERR:
193 | break; // noop
194 | default:
195 | alert('An error occurred reading this file.' + e);
196 | }
197 | }
198 |
199 | }
--------------------------------------------------------------------------------
/app/capture-image.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
20 |
21 |
22 |
23 |
24 |
--------------------------------------------------------------------------------
/app/capture-image.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | const electron = require('electron');
4 | const remote = electron.remote;
5 | const nativeImage = electron.nativeImage
6 | const fs = require('fs');
7 |
8 | let win = remote.getCurrentWindow();
9 |
10 | var figure = document.getElementById('figure');
11 | function load (photo, style, filename, longest, orientation) {
12 | while (figure.firstChild) {
13 | figure.removeChild(figure.firstChild);
14 | }
15 |
16 | console.time('load');
17 |
18 | if (longest) figure.style.cssText = `max-width: ${longest}px; max-height: ${longest}px;`;
19 |
20 | var img = new Image();
21 | img.src = photo;
22 |
23 | figure.appendChild(img);
24 | figure.className = style;
25 |
26 | console.log('orientation', arguments);
27 |
28 | if (orientation) figure.classList.add(`orientation${orientation}`);
29 |
30 |
31 | img.onload = () => {
32 | console.timeEnd('load');
33 | console.time('raf');
34 |
35 | // if (!longest) {
36 | // longest = 'auto';
37 | // figure.style.cssText = `max-width: ${longest}px; max-height: ${longest}px;`;
38 | // }
39 |
40 | if (orientation > 1) {
41 | let b = img.getBoundingClientRect();
42 | console.log(b);
43 |
44 | figure.style.cssText += `
45 | transform:
46 | translateX(${-b.left}px)
47 | translateY(${-b.top}px)
48 | rotate(-90deg)
49 |
50 | ;
51 | `;
52 | // if (b.width > b.height) {
53 | // var diff = b.height - b.width / 2;
54 | // figure.style.cssText += `
55 | // transform:
56 | // translateY(${diff}px)
57 | // translateX(${-diff}px)
58 | // rotate(-90deg);
59 | // `;
60 | // }
61 | }
62 |
63 | var count = 0;
64 | var wait = function() {
65 | count++;
66 | if (count < 3 || win.isLoading()) {
67 | // not sure if this actually is useful.
68 | // 'did-stop-loading' perhaps naturalWidth can give clues too?
69 | return requestAnimationFrame(wait);
70 | }
71 | console.timeEnd('raf')
72 | console.log(filename, img);
73 | save(filename, img);
74 | }
75 | requestAnimationFrame(wait);
76 | };
77 | }
78 |
79 | function save(filename, img) {
80 | let b = img.getBoundingClientRect();
81 |
82 | let o = {
83 | x: b.left,
84 | y: b.top,
85 | width: b.width,
86 | height: b.height
87 | };
88 |
89 | console.log(o);
90 |
91 | // caveat - bounds must entirely be inside chrome's viewport!!
92 | win.capturePage(o, img => {
93 | // console.log(img.getSize())
94 | // fs.writeFileSync('screenshot.png', img.toPng());
95 | fs.writeFileSync(filename, img.toJpeg(98));
96 |
97 | // turn this off if you need debugging
98 | win.close();
99 |
100 | // // could make an ipc call here instead.
101 | // electron.ipcRenderer.send('signal', 'done');
102 | });
103 | }
--------------------------------------------------------------------------------
/app/cssgram.min.css:
--------------------------------------------------------------------------------
1 | .aden{-webkit-filter:hue-rotate(-20deg) contrast(.9) saturate(.85) brightness(1.2);filter:hue-rotate(-20deg) contrast(.9) saturate(.85) brightness(1.2)}.aden::after{background:-webkit-linear-gradient(left,rgba(66,10,14,.2),transparent);background:linear-gradient(to right,rgba(66,10,14,.2),transparent);mix-blend-mode:darken}.inkwell{-webkit-filter:sepia(.3) contrast(1.1) brightness(1.1) grayscale(1);filter:sepia(.3) contrast(1.1) brightness(1.1) grayscale(1)}.perpetua::after{background:-webkit-linear-gradient(top,#005b9a,#e6c13d);background:linear-gradient(to bottom,#005b9a,#e6c13d);mix-blend-mode:soft-light;opacity:.5}.reyes{-webkit-filter:sepia(.22) brightness(1.1) contrast(.85) saturate(.75);filter:sepia(.22) brightness(1.1) contrast(.85) saturate(.75)}.reyes::after{background:#efcdad;mix-blend-mode:soft-light;opacity:.5}.gingham{-webkit-filter:brightness(1.05) hue-rotate(-10deg);filter:brightness(1.05) hue-rotate(-10deg)}.gingham::after{background:-webkit-linear-gradient(left,rgba(66,10,14,.2),transparent);background:linear-gradient(to right,rgba(66,10,14,.2),transparent);mix-blend-mode:darken}.toaster{-webkit-filter:contrast(1.5) brightness(.9);filter:contrast(1.5) brightness(.9)}.toaster::after{background:-webkit-radial-gradient(circle,#804e0f,#3b003b);background:radial-gradient(circle,#804e0f,#3b003b);mix-blend-mode:screen}.walden{-webkit-filter:brightness(1.1) hue-rotate(-10deg) sepia(.3) saturate(1.6);filter:brightness(1.1) hue-rotate(-10deg) sepia(.3) saturate(1.6)}.walden::after{background:#04c;mix-blend-mode:screen;opacity:.3}.hudson{-webkit-filter:brightness(1.2) contrast(.9) saturate(1.1);filter:brightness(1.2) contrast(.9) saturate(1.1)}.hudson::after{background:-webkit-radial-gradient(circle,#a6b1ff 50%,#342134);background:radial-gradient(circle,#a6b1ff 50%,#342134);mix-blend-mode:multiply;opacity:.5}.earlybird{-webkit-filter:contrast(.9) sepia(.2);filter:contrast(.9) sepia(.2)}.earlybird::after{background:-webkit-radial-gradient(circle,#d0ba8e 20%,#360309 85%,#1d0210 100%);background:radial-gradient(circle,#d0ba8e 20%,#360309 85%,#1d0210 100%);mix-blend-mode:overlay}.mayfair{-webkit-filter:contrast(1.1) saturate(1.1);filter:contrast(1.1) saturate(1.1)}.mayfair::after{background:-webkit-radial-gradient(40% 40%,circle,rgba(255,255,255,.8),rgba(255,200,200,.6),#111 60%);background:radial-gradient(circle at 40% 40%,rgba(255,255,255,.8),rgba(255,200,200,.6),#111 60%);mix-blend-mode:overlay;opacity:.4}.lofi{-webkit-filter:saturate(1.1) contrast(1.5);filter:saturate(1.1) contrast(1.5)}.lofi::after{background:-webkit-radial-gradient(circle,transparent 70%,#222 150%);background:radial-gradient(circle,transparent 70%,#222 150%);mix-blend-mode:multiply}._1977{-webkit-filter:contrast(1.1) brightness(1.1) saturate(1.3);filter:contrast(1.1) brightness(1.1) saturate(1.3)}._1977:after{background:rgba(243,106,188,.3);mix-blend-mode:screen}.brooklyn{-webkit-filter:contrast(.9) brightness(1.1);filter:contrast(.9) brightness(1.1)}.brooklyn::after{background:-webkit-radial-gradient(circle,rgba(168,223,193,.4) 70%,#c4b7c8);background:radial-gradient(circle,rgba(168,223,193,.4) 70%,#c4b7c8);mix-blend-mode:overlay}.xpro2{-webkit-filter:sepia(.3);filter:sepia(.3)}.xpro2::after{background:-webkit-radial-gradient(circle,#e6e7e0 40%,rgba(43,42,161,.6) 110%);background:radial-gradient(circle,#e6e7e0 40%,rgba(43,42,161,.6) 110%);mix-blend-mode:color-burn}.nashville{-webkit-filter:sepia(.2) contrast(1.2) brightness(1.05) saturate(1.2);filter:sepia(.2) contrast(1.2) brightness(1.05) saturate(1.2)}.nashville::after{background:rgba(0,70,150,.4);mix-blend-mode:lighten}.nashville::before{background:rgba(247,176,153,.56);mix-blend-mode:darken}.lark{-webkit-filter:contrast(.9);filter:contrast(.9)}.lark::after{background:rgba(242,242,242,.8);mix-blend-mode:darken}.lark::before{background:#22253f;mix-blend-mode:color-dodge}.moon{-webkit-filter:grayscale(1) contrast(1.1) brightness(1.1);filter:grayscale(1) contrast(1.1) brightness(1.1)}.moon::before{background:#a0a0a0;mix-blend-mode:soft-light}.moon::after{background:#383838;mix-blend-mode:lighten}.clarendon{-webkit-filter:contrast(1.2) saturate(1.35);filter:contrast(1.2) saturate(1.35)}.clarendon:before{background:rgba(127,187,227,.2);mix-blend-mode:overlay}._1977:after,._1977:before,.aden:after,.aden:before,.brooklyn:after,.brooklyn:before,.clarendon:after,.clarendon:before,.earlybird:after,.earlybird:before,.gingham:after,.gingham:before,.hudson:after,.hudson:before,.inkwell:after,.inkwell:before,.lark:after,.lark:before,.lofi:after,.lofi:before,.mayfair:after,.mayfair:before,.moon:after,.moon:before,.nashville:after,.nashville:before,.perpetua:after,.perpetua:before,.reyes:after,.reyes:before,.toaster:after,.toaster:before,.walden:after,.walden:before,.willow:after,.willow:before,.xpro2:after,.xpro2:before{content:'';display:block;height:100%;width:100%;top:0;left:0;position:absolute;pointer-events:none}._1977,.aden,.brooklyn,.clarendon,.earlybird,.gingham,.hudson,.inkwell,.lark,.lofi,.mayfair,.moon,.nashville,.perpetua,.reyes,.toaster,.walden,.willow,.xpro2{position:relative}._1977 img,.aden img,.brooklyn img,.clarendon img,.earlybird img,.gingham img,.hudson img,.inkwell img,.lark img,.lofi img,.mayfair img,.moon img,.nashville img,.perpetua img,.reyes img,.toaster img,.walden img,.willow img,.xpro2 img{width:100%;z-index:1}._1977:before,.aden:before,.brooklyn:before,.clarendon:before,.earlybird:before,.gingham:before,.hudson:before,.inkwell:before,.lark:before,.lofi:before,.mayfair:before,.moon:before,.nashville:before,.perpetua:before,.reyes:before,.toaster:before,.walden:before,.willow:before,.xpro2:before{z-index:2}._1977:after,.aden:after,.brooklyn:after,.clarendon:after,.earlybird:after,.gingham:after,.hudson:after,.inkwell:after,.lark:after,.lofi:after,.mayfair:after,.moon:after,.nashville:after,.perpetua:after,.reyes:after,.toaster:after,.walden:after,.willow:after,.xpro2:after{z-index:3}.willow{-webkit-filter:grayscale(.5) contrast(.95) brightness(.9);filter:grayscale(.5) contrast(.95) brightness(.9)}.willow::before{background-color:radial-gradient(40%,circle,#d4a9af 55%,#000 150%);mix-blend-mode:overlay}.willow::after{background-color:#d8cdcb;mix-blend-mode:color}
--------------------------------------------------------------------------------
/app/index.html:
--------------------------------------------------------------------------------
1 |
4 |
5 |
6 |
7 | Lightbroom Preview
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 | Long side:
40 |
41 |
42 |
43 |
44 |
45 |
46 | Filmstrip:
47 |
48 |
49 |
50 |
51 |
52 |
53 | Panels:
54 |
55 |
56 |
57 |
58 |
59 |
60 |
61 | Drag photos into the app to start.
62 |
63 |
64 |
65 |
66 |
67 |
68 |
69 |
70 |
71 |
72 |
73 |
77 |
78 |
79 |
80 |
81 |
82 |
83 |
84 |
85 |
86 |
87 |
88 |
89 |
90 |
91 |
92 |
95 |
96 |
97 |
98 |
99 |
100 |
101 |
--------------------------------------------------------------------------------
/app/index.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 | const electron = require('electron');
3 | const app = electron.app;
4 |
5 | // prevent window being garbage collected
6 | let mainWindow;
7 |
8 | function onClosed() {
9 | mainWindow = null;
10 | }
11 |
12 | function createMainWindow() {
13 | const win = new electron.BrowserWindow({
14 | width: 600,
15 | height: 400
16 | });
17 |
18 | win.loadURL(`file://${__dirname}/index.html`);
19 | win.on('closed', onClosed);
20 |
21 | // win.webContents.openDevTools();
22 |
23 | return win;
24 | }
25 |
26 | app.on('window-all-closed', () => {
27 | if (process.platform !== 'darwin') {
28 | app.quit();
29 | }
30 | });
31 |
32 | app.on('activate-with-no-open-windows', () => {
33 | if (!mainWindow) {
34 | mainWindow = createMainWindow();
35 | }
36 | });
37 |
38 | app.on('ready', () => {
39 | mainWindow = createMainWindow();
40 | });
41 |
--------------------------------------------------------------------------------
/app/lightbroom.css:
--------------------------------------------------------------------------------
1 | body {
2 | font-family: 'Lato', sans-serif;
3 | background-color: #333740;
4 | color: #ddd;
5 | font-size: 13px;
6 | margin: 0;
7 | padding: 0;
8 | outline: none;
9 |
10 | display: flex;
11 | flex-direction: column;
12 | }
13 |
14 | html, body {
15 | height: 100%;
16 | }
17 |
18 | #topbar {
19 | background: #555862;
20 | box-shadow: 0px 1px 0px 0px #24272D;
21 | width: 100%;
22 | height: 50px;
23 | display: flex;
24 | }
25 |
26 | #save-options {
27 | transition: width 400ms ease-out;
28 | width: 90px;
29 | display: inline-block;
30 | overflow: hidden;
31 | }
32 |
33 | #save-options:hover {
34 | width: 670px;
35 | }
36 |
37 | .peek {
38 | max-height: 120px;
39 | max-width: 120px;
40 | float: left;
41 | margin: 2px;
42 | }
43 |
44 | .nofilter img {
45 | width: 100%;
46 | }
47 |
48 | .selected-effect {
49 | border: 2px solid #ccc;
50 | box-sizing: border-box;
51 | }
52 |
53 | .selected-effect:hover {
54 | border: 2px solid #fff;
55 | }
56 |
57 | #filter-wrapper {
58 | overflow: scroll;
59 | background-color: #23262C;
60 | box-shadow: 0px 1px 0px 0px rgba(255,255,255,0.30);
61 | font-size: 13px;
62 | transition: height 250ms;
63 | height: 128px;
64 | }
65 |
66 | #filters {
67 | padding: 14px;
68 | display: flex;
69 | }
70 |
71 | button {
72 | font-family: 'Lato', sans-serif;
73 | padding: 10px;
74 | margin: 4px 4px 14px 4px;
75 | background-image: linear-gradient(-180deg, #7B7E87 0%, #555862 100%);
76 | border: 1px solid #292B31;
77 | box-shadow: 0px 1px 5px 0px rgba(0, 0, 0, 0.30), inset 0px 1px 0px 0px rgba(255, 255, 255, 0.60);
78 | border-radius: 4px;
79 | color: #eaeaea;
80 | font-size: 13px;
81 |
82 | text-overflow: ellipsis;
83 | white-space: nowrap;
84 | outline: none;
85 | }
86 |
87 | .effect {
88 | width: 130px;
89 | display: block;
90 | text-align: center;
91 | font-weight: bold;
92 | display: block;
93 | }
94 |
95 | button:active, button.active {
96 | /*box-shadow: 0px 1px 1px 0px rgba(255, 255, 255, 0.30), inset 0px 0px 4px 0px rgba(0, 0, 0, 0.80);*/
97 | background: #23262C;
98 | border: 1px solid #000000;
99 | box-shadow: 0px 1px 1px 0px rgba(255,255,255,0.50);
100 | }
101 |
102 | button:hover {
103 | background-image: -webkit-linear-gradient(#BDC2D0 0%, #656874 100%);
104 | background-image: -o-linear-gradient(#BDC2D0 0%, #656874 100%);
105 | background-image: linear-gradient(#BDC2D0 0%, #656874 100%);
106 | }
107 |
108 | img {
109 | border: 1px solid transparent;
110 | }
111 |
112 | img:hover {
113 | border: 1px solid #ddd;
114 | }
115 |
116 | #contents {
117 | flex: 1;
118 | display: flex;
119 | flex-direction: column;
120 | }
121 |
122 | #loupe {
123 | /*height: 46%;*/
124 | flex: 1;
125 | overflow: auto;
126 | }
127 |
128 | #big {
129 | /*width: 590px;
130 | height: 390px;
131 | */
132 | /*max-height: 590px;
133 | max-width: 590px;*/
134 | }
135 |
136 | .dotted-border {
137 | width: 360px;
138 | height: 230px;
139 | border: 2px dotted #676B78;
140 | border-radius: 4px;
141 | }
142 |
143 | .txt-drag-your-photos-here {
144 | font-size: 15px;
145 | color: #898D9B;
146 | }
147 |
148 | #sliders {
149 | right: 0px;
150 | width: 220px;
151 | position: absolute;
152 | background:rgba(0, 0, 0, 0.30);
153 | z-index: 10;
154 | }
155 |
156 | #preview {
157 | height: 39%;
158 | width: 100%;
159 |
160 | /*position: absolute;
161 | overflow: auto;*/
162 | bottom: 0;
163 | border-top: 1px solid #999;
164 | transition: 350ms ease-out height;
165 | /*background: #333;*/
166 | z-index: 2;
167 |
168 | display: flex;
169 | flex-direction: column;
170 | }
171 |
172 | #filmstrip img {
173 | transition: height 350ms ease-out;
174 | }
175 |
176 | #filmstrip {
177 | /*width: 100%;
178 | height: 100%;*/
179 | flex: 1;
180 | overflow: auto;
181 |
182 | display: flex;
183 | flex-direction: row;
184 | flex-wrap: wrap;
185 |
186 | align-content: flex-start;
187 | }
188 |
189 | #preview-slider {
190 | display: block;
191 | width: 100%;
192 | height: 8px;
193 | background: #000;
194 | cursor: ns-resize;
195 | }
196 |
197 | .no-transition {
198 | transition: none !important;
199 | }
200 |
201 | .checkbox {
202 | /*background: deeppink;*/
203 | /*width: 40px;
204 | height: 40px;*/
205 | position: absolute;
206 | right: 0;
207 | /*display: inline-block;*/
208 | z-index: 10;
209 | /*opacity: 0.2;*/
210 |
211 | }
212 |
213 | .checkbox label:hover {
214 | opacity: 1;
215 | background: rgba(0,0,0,0.50);
216 | }
217 |
218 | .checkbox label {
219 | display: inline-block;
220 | position: relative;
221 | font-size: 2em;
222 | vertical-align: center;
223 | text-align: center;
224 | color: rgba(255, 255, 255, 0.5);
225 | cursor: pointer;
226 | transition: color 0.3s;
227 | border: 2px solid rgba(255, 255, 255, 0.8);
228 | width: 1.3em;
229 | height: 1.3em;
230 | opacity: 0.2;
231 | border-radius: 0;
232 | transition: opacity 0.2s, border-radius 0.2s, border 0.25s;
233 | margin: 4px;
234 | }
235 |
236 | .checkbox input[type="checkbox"],
237 | .checkbox input[type="radio"] {
238 | display: none;
239 | }
240 |
241 | .checkbox .checked {
242 | opacity: 1;
243 | color: rgba(255, 255, 255, 1);
244 | border: 2px solid rgba(255, 255, 255, 0.8);
245 | border-radius: 50%;
246 | }
247 |
248 | #filmstrip > div {
249 | display: inline-block;
250 | margin: 5px;
251 | position: relative;
252 | }
253 |
254 | .pic {
255 | margin: 0;
256 | }
257 |
258 | .pic img, .effect img {
259 | border-radius: 4px;
260 | }
--------------------------------------------------------------------------------
/app/lightbroom.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | var MAX_EFFECT_PREVIEW_LENGTH = 240;
4 | var MAX_BIG_PREVIEW_LENGTH = 1600; // || 590;
5 | var FILMSTRIP_THUMBNAIL_LENGTH = 390;;
6 |
7 | var opener = new ImageOpener(processImage);
8 |
9 | var preview = document.getElementById('preview');
10 | var filters = document.getElementById('filters');
11 | var big = document.getElementById('big'), bigimg;
12 | var filmstrip = document.getElementById('filmstrip');
13 |
14 | var selectionMode = false;
15 |
16 | function toggleSelectMode(b) {
17 | selectionMode = !selectionMode;
18 | b.className = selectionMode ? 'active' : '';
19 | }
20 |
21 | var styles = [
22 | "Nofilter",
23 | "Aden",
24 | "Inkwell",
25 | "Perpetua",
26 | "Reyes",
27 | "Gingham",
28 | "Toaster",
29 | "Walden",
30 | "Hudson",
31 | "Earlybird",
32 | "Mayfair",
33 | "Lofi",
34 | "_1977",
35 | "Brooklyn",
36 | "Xpro2",
37 | "Nashville",
38 | "Lark",
39 | "Moon",
40 | "Clarendon",
41 | "Willow"
42 | ];
43 |
44 | var currentStyle = 'nofilter';
45 | var saved;
46 | var filterItems = new Map();
47 |
48 | var filenames;
49 | var selectedPhoto;
50 |
51 | var photos = new PhotoList();
52 |
53 | setup();
54 |
55 | function setup() {
56 | // add styled effect
57 | styles.forEach( function(s) {
58 | var div = document.createElement('div');
59 |
60 | var b = document.createElement('b');
61 | b.className = 'effect';
62 |
63 | b.onclick = function() {
64 | var last = document.querySelector('.selected-effect');
65 | if (last) last.classList.remove('selected-effect');
66 | b.querySelector('img').classList.add('selected-effect');
67 |
68 | saved = currentStyle;
69 | switchStyle(s, true);
70 | }
71 |
72 | b.onmouseover = function() {
73 | saved = currentStyle;
74 | switchStyle(s);
75 | }
76 |
77 | b.onmouseout = function() {
78 | switchStyle(saved);
79 | }
80 |
81 | div.appendChild(b);
82 |
83 | var figure = document.createElement('figure');
84 | b.appendChild(figure);
85 | b.innerHTML += s;
86 |
87 | filterItems.set(s, div);
88 | filters.appendChild(div);
89 | } );
90 |
91 | // setup big image preview
92 |
93 | // var x = 0;
94 |
95 | // var tmp;
96 | // var save;
97 |
98 | // var img = big;
99 |
100 | // img.onmouseover = function(e) {
101 | // save = currentStyle;
102 | // }
103 |
104 | // img.onmousemove = function(e) {
105 | // // code here to reimplement scrubbing behaviour
106 | // // (potatoe prototype6)
107 | // }
108 |
109 | // img.onmouseout = function(e) {
110 | // if (!bigimg) return;
111 | // tmp = bigimg.classList[1];
112 | // bigimg.classList.remove(tmp);
113 | // bigimg.classList.add(currentStyle);
114 | // }
115 |
116 | // img.onmousedown = function(e) {
117 | // save = bigimg.classList[1];
118 | // switchStyle(save);
119 | // }
120 | }
121 |
122 |
123 | function switchStyle(s, commit) {
124 | var old = currentStyle;
125 |
126 | // photos.selection.forEach( p => {
127 | // p.effect = s;
128 | // });
129 |
130 | // photosDomSync();
131 |
132 | var pics = document.querySelectorAll('.pic, .big')
133 |
134 | for (var i = 0; i < pics.length; i ++) {
135 | var p = pics[i];
136 | p.classList.remove(old);
137 | p.classList.add(s);
138 | }
139 |
140 | currentStyle = s;
141 | }
142 |
143 | function previewBig(oimg, photo, orientation) {
144 | selectedPhoto = photo;
145 |
146 | var img = scaleImage(oimg, MAX_BIG_PREVIEW_LENGTH, orientation);
147 |
148 | var child;
149 | while (child = big.childNodes[0]) {
150 | child.remove();
151 | }
152 |
153 | var n = document.createElement('figure');
154 | n.appendChild(img.cloneNode());
155 | n.classList.add('big');
156 | n.classList.add(currentStyle);
157 | bigimg = n;
158 |
159 | big.appendChild(n);
160 |
161 | // img = scaleImage(oimg, maxLength, orientation);
162 |
163 | img = scaleImage(img, MAX_EFFECT_PREVIEW_LENGTH);
164 |
165 | img.onload = () => {
166 |
167 | styles.forEach(function(s) {
168 | var m = filterItems.get(s).querySelector('figure');
169 |
170 | while (child = m.firstChild) {
171 | child.remove();
172 | }
173 |
174 | m.appendChild(img.cloneNode());
175 |
176 | while (m.classList.length) {
177 | m.classList.remove(m.classList[0]);
178 | }
179 | m.classList.add('peek');
180 | m.classList.add(s);
181 |
182 | });
183 |
184 | }
185 |
186 | }
187 |
188 | function scaleImage(img, maxLength, orientation) {
189 | var canvas = getCanvas(img, maxLength, orientation);
190 |
191 | var image = new Image();
192 | image.src = canvas.toDataURL("image/png");
193 |
194 | return image;
195 | }
196 |
197 | function getCanvas(img, maxLength, orientation) {
198 | var imgWidth = img.naturalWidth;
199 | var imgHeight = img.naturalHeight;
200 |
201 | // can rotate here....
202 |
203 | var scale = imgWidth > imgHeight ? maxLength / imgWidth : maxLength / imgHeight;
204 |
205 | var canvas = document.createElement('canvas');
206 | var ctx = canvas.getContext('2d');
207 |
208 | var targetWidth = imgWidth * scale;
209 | var targetHeight = imgHeight * scale;
210 |
211 | canvas.width = targetWidth;
212 | canvas.height = targetHeight;
213 | if (orientation > 4) {
214 | canvas.width = targetHeight;
215 | canvas.height = targetWidth;
216 | }
217 |
218 | var width = targetWidth;
219 | var height = targetHeight;
220 |
221 | switch (orientation) {
222 | case 2:
223 | // horizontal flip
224 | ctx.translate(width, 0);
225 | ctx.scale(-1, 1);
226 | break;
227 | case 3:
228 | // 180° rotate left
229 | ctx.translate(width, height);
230 | ctx.rotate(Math.PI);
231 | break;
232 | case 4:
233 | // vertical flip
234 | ctx.translate(0, height);
235 | ctx.scale(1, -1);
236 | break;
237 | case 5:
238 | // vertical flip + 90 rotate right
239 | ctx.rotate(0.5 * Math.PI);
240 | ctx.scale(1, -1);
241 | break;
242 | case 6:
243 | // 90° rotate right
244 | ctx.rotate(0.5 * Math.PI);
245 | ctx.translate(0, -height);
246 | break;
247 | case 7:
248 | // horizontal flip + 90 rotate right
249 | ctx.rotate(0.5 * Math.PI);
250 | ctx.translate(width, -height);
251 | ctx.scale(-1, 1);
252 | break;
253 | case 8:
254 | // 90° rotate left
255 | ctx.rotate(-0.5 * Math.PI);
256 | ctx.translate(-width, 0);
257 | break;
258 | }
259 |
260 | ctx.drawImage(img, 0, 0, targetWidth, targetHeight);
261 |
262 | return canvas;
263 | }
264 |
265 | function processImage(oImg, path, i) {
266 | EXIF.getData(oImg, function() {
267 | var exif = EXIF.getAllTags(oImg);
268 | processImage2(oImg, path, i, exif);
269 | });
270 | }
271 |
272 | function processImage2(oImg, path, i, exif) {
273 | var orientation = exif.Orientation;
274 | var photo = new Photo(path, oImg, orientation);
275 | photos.add( photo );
276 |
277 | // console.log('process image');
278 |
279 | // var canvas = getCanvas(img, 550);
280 | // inlineSVGImg(canvas.toDataURL(), canvas.width, canvas.height);
281 |
282 | if (photos.count() == 1) {
283 | previewBig(oImg, photo, orientation);
284 | }
285 |
286 | var img = scaleImage(oImg, FILMSTRIP_THUMBNAIL_LENGTH, orientation);
287 |
288 | var div = document.createElement('div');
289 | div.dataset.path = path;
290 |
291 | var checkbox = document.createElement('div');
292 | checkbox.className = 'checkbox';
293 |
294 | var lb = document.createElement('label');
295 | lb.innerHTML = '✔';
296 |
297 | var cb = document.createElement('input');
298 | cb.type = 'checkbox';
299 | cb.onchange = function() {
300 | if (cb.checked) {
301 | lb.classList.add('checked');
302 | photos.selection.add( photo );
303 | } else {
304 | lb.classList.remove('checked');
305 | photos.selection.delete( photo );
306 | }
307 | }
308 |
309 | lb.appendChild(cb);
310 |
311 | // ✔☑✓✓
312 | checkbox.appendChild(lb);
313 |
314 | var figure = document.createElement('figure');
315 | figure.classList.add('pic');
316 | figure.classList.add(currentStyle);
317 | figure.appendChild(img);
318 |
319 | div.appendChild(checkbox);
320 | div.appendChild(figure);
321 |
322 | // div.innerHTML += path;
323 |
324 | filmstrip.appendChild(div);
325 | figure.scrollIntoView(false, 200);
326 |
327 | img.onclick = function() {
328 | if (selectionMode) {
329 | cb.checked = !cb.checked;
330 | cb.onchange();
331 | } else {
332 | previewBig(oImg, photo, orientation);
333 | }
334 | }
335 |
336 | /*
337 | photos.remove(photo);
338 | photosDomSync();
339 | */
340 |
341 |
342 | sizeThumbnails(lastThumbnailSize);
343 | }
344 |
345 | var toggled = 0;
346 |
347 | /* Actions */
348 | function togglePreviewSlider(x) {
349 | toggled = x !== undefined ? x : toggled;
350 | switch (toggled) {
351 | case 0:
352 | preview.style.height = '100%';
353 | break;
354 | case 1:
355 | preview.style.height = '300px';
356 | break;
357 | case 2:
358 | preview.style.height = '0px';
359 | break;
360 | }
361 |
362 | toggled = (toggled + 1) % 3;
363 | }
364 |
365 | var lastThumbnailSize = 200;
366 | function sizeThumbnails(size) {
367 | lastThumbnailSize = 200;
368 | Array.from(document.querySelectorAll('#preview figure')).forEach(
369 | d => {
370 | var i = d.querySelector('img');
371 | i.style.width = 'auto';
372 | // i.style.height = `100%`;
373 | i.style.height = `${size}px`;
374 | // d.style.maxWidth =`${size}px`;
375 | // d.style.maxHeight = `${size * 0.67}px`;
376 | // d.style.maxHeight = `auto`;
377 | // d.style.maxHeight = `${size}px`;
378 | }
379 | )
380 | }
381 |
382 | function clearThumbnails() {
383 | var proceed = confirm('Remove all photos?');
384 | if (!proceed) return;
385 | photos.empty();
386 | photosDomSync();
387 | }
388 |
389 | function removeSelection() {
390 | var proceed = confirm('Remove ' + photos.selection.size + ' photos?');
391 | if (!proceed) return;
392 | photos.selection.forEach(photos.remove.bind(photos));
393 | photosDomSync();
394 | }
395 |
396 | var toggleSelectAll = false;
397 |
398 | function selectAll() {
399 | // photos.list.forEach(photos.selection.add.bind(photos));
400 | // photosDomSync();
401 |
402 | toggleSelectAll = !toggleSelectAll;
403 |
404 | Array.from(document.querySelectorAll('#filmstrip input')).forEach(
405 | cb => {
406 | cb.checked = toggleSelectAll;
407 | cb.onchange();
408 | }
409 | )
410 | }
411 |
412 | function photosDomSync() {
413 | var dom = Array.from(document.querySelectorAll('[data-path]'))
414 |
415 | dom.forEach( d => {
416 | var match = photos.filter(d.dataset.path);
417 |
418 | // remove
419 | if (! match.length ) {
420 | return d.remove();
421 | }
422 |
423 | // update
424 | var img = d.querySelector('.pic');
425 | img.className = 'pic ' + match[0].effect;
426 | } );
427 | }
428 |
429 | var effectsTray = true;
430 | var filterWrapper = document.getElementById('filter-wrapper');
431 |
432 | function toggleEffects(button) {
433 | if (effectsTray) {
434 | filterWrapper.style.height = '0px';
435 | effectsTray = false;
436 | button.className = '';
437 | } else {
438 | filterWrapper.style.height = '128px';
439 | effectsTray = true;
440 | button.className = 'active';
441 | }
442 | }
443 |
444 | var slider = document.getElementById('preview-slider');
445 | slider.onmousedown = function() {
446 | var moved = false;
447 |
448 | var onmove = function(e) {
449 | moved = true;
450 | var x = window.innerHeight - e.screenY;
451 |
452 | preview.style.height = x + 'px';
453 | preview.style.transition = 'none';
454 | e.stopPropagation();
455 | e.preventDefault();
456 | // preview.classList.add('no-transition');
457 | };
458 |
459 | var onup = function() {
460 | if (!moved) {
461 | togglePreviewSlider();
462 | }
463 | // preview.classList.remove('no-transition');
464 | preview.style.transition = '';
465 |
466 | window.removeEventListener('mousemove', onmove);
467 | window.removeEventListener('mouseup', onup);
468 | }
469 |
470 | window.addEventListener('mousemove', onmove);
471 | window.addEventListener('mouseup', onup);
472 | }
473 |
474 | var FILTERS = {
475 | // type, min, max, default, step
476 | brightness: [0, 4, 1, 0.001],
477 | contrast: [0, 4, 1, 0.0001],
478 | sepia: [0, 1, 0, 0.0001],
479 | grayscale: [0, 1, 0, 0.001],
480 | saturate: [0, 2, 1, 0.0001],
481 | blur: [0, 10, 0, 1, 'px'],
482 | opacity: [0, 1, 1, 0.001],
483 | // 'drop-shadow': [0, 10, 1, 0.001, 'px'],
484 | 'hue-rotate': [0, 360, 0, 0.5, 'deg'],
485 | invert: [0, 1, 0, 0.001]
486 | };
487 |
488 | var theSliders = {};
489 |
490 | Object.keys(FILTERS).map( k => {
491 | var values = FILTERS[k];
492 | var filterSlider = document.createElement('input');
493 | filterSlider.type = 'range';
494 | filterSlider.min = values[0];
495 | filterSlider.max = values[1];
496 | filterSlider.value = values[2];
497 | filterSlider.step = values[3];
498 |
499 | filterSlider.onmousemove =
500 | filterSlider.onchange = updateFilters;
501 |
502 |
503 | theSliders[k] = filterSlider;
504 |
505 | // sliders.appendChild(document.createElement('br'));
506 | sliders.appendChild(filterSlider);
507 | sliders.appendChild(document.createTextNode(k));
508 |
509 | })
510 |
511 | var before = true;
512 |
513 | function updateFilters() {
514 | var img = big.querySelector('img');
515 |
516 | var css = '-webkit-filter: ';
517 |
518 | Object.keys(theSliders).map( k => {
519 | var input = theSliders[k];
520 | var values = FILTERS[k];
521 | if (input.value !== values[2]) css += ` ${k}(${input.value}${values[4] || ''})`;
522 | });
523 |
524 | console.log(css);
525 |
526 | if (before)
527 | img.style.cssText = css;
528 | else
529 | big.style.cssText = css;
530 | }
531 |
532 | function brightness(input) {
533 | console.log(input.value);
534 |
535 |
536 | // big.style.cssText = `-webkit-filter: brightness(${input.value})`;
537 | }
538 |
539 | function contrast(input) {
540 | console.log(input.value);
541 | var img = big.querySelector('img');
542 | img.style.cssText = `-webkit-filter: contrast(${input.value})`;
543 | // big.style.cssText = `-webkit-filter: brightness(${input.value})`;
544 | }
--------------------------------------------------------------------------------
/app/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "lightbroom",
3 | "version": "0.3.0",
4 | "description": "photo workflow software",
5 | "main": "index.js",
6 | "directories": {
7 | "test": "tests"
8 | },
9 | "scripts": {
10 | "bumpcssgram": "rm cssgram.min.css && wget https://github.com/una/CSSgram/raw/master/site/css/cssgram.min.css"
11 | },
12 | "repository": {
13 | "type": "git",
14 | "url": "git+https://github.com/zz85/lightbroom.git"
15 | },
16 | "keywords": [
17 | "photo",
18 | "filters",
19 | "effect",
20 | "ps",
21 | "webgl"
22 | ],
23 | "author": "Joshua Koo",
24 | "license": "ISC",
25 | "bugs": {
26 | "url": "https://github.com/zz85/lightbroom/issues"
27 | },
28 | "homepage": "https://github.com/zz85/lightbroom#readme",
29 | "electronVersion": "0.36.2",
30 | "devDependencies": {
31 | "cssgram": "^0.1.3",
32 | "jimp": "^0.2.21"
33 | },
34 | "dependencies": {
35 | "exif-js": "^2.1.1"
36 | }
37 | }
38 |
--------------------------------------------------------------------------------
/app/photo.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | // model representation of photos to load
4 |
5 | function PhotoList() {
6 | this.list = []; // new Set();
7 |
8 | this.selection = new Set();
9 | }
10 |
11 | PhotoList.prototype.add = function(photo) {
12 | this.list.push(photo);
13 | this.save();
14 | // this.selection.add(photo);
15 | }
16 |
17 | PhotoList.prototype.remove = function(photo) {
18 | var i = this.list.indexOf(photo);
19 | this.list.splice(i, 1);
20 |
21 | this.selection.delete(photo);
22 | this.save();
23 | }
24 |
25 | PhotoList.prototype.count = function() {
26 | return this.list.length;
27 | }
28 |
29 | PhotoList.prototype.filenames = function() {
30 | return this.list.map( p => p.filename );
31 | }
32 |
33 | PhotoList.prototype.items = function() {
34 | return Array.from(this.list);
35 | }
36 |
37 | PhotoList.prototype.toJSON = function() {
38 | this.list.map( p => { return {
39 | filename: p.filename,
40 | style: p.style
41 | }} )
42 | }
43 |
44 | PhotoList.prototype.save = function() {
45 | localStorage.lastLoad = JSON.stringify(this.filenames());
46 | // localStorage.lastLoad = JSON.stringify(this.toJSON());
47 | }
48 |
49 | PhotoList.prototype.load = function() {
50 | // TODO
51 | var lastLoad = localStorage.lastLoad;
52 | if (lastLoad) {
53 | var list = JSON.parse(lastLoad);
54 | list.forEach( json => {
55 | this.list.push(
56 | new Photo()
57 | )
58 | } );
59 | }
60 | }
61 |
62 | PhotoList.prototype.empty = function() {
63 | this.list = [];
64 | this.save();
65 |
66 | this.selection.clear()
67 | }
68 |
69 | PhotoList.prototype.exists = function(path) {
70 | return this.list.some( p => p.filename === path );
71 | }
72 |
73 | PhotoList.prototype.filter = function(path) {
74 | return this.list.filter( p => p.filename === path );
75 | }
76 |
77 | function Photo(filename, img, orientation) {
78 | this.filename = filename;
79 | this.img = img;
80 | this.orientation = orientation;
81 |
82 | this.effect = null;
83 | this.adjustment = '';
84 | }
--------------------------------------------------------------------------------
/app/svg-image.js:
--------------------------------------------------------------------------------
1 | function inlineSVGImg(data, width, height) {
2 | width = width || 20;
3 | hight = height || 20;
4 | var div = document.createElement('div');
5 |
6 | var svg = ``;
143 |
144 | console.log(svg);
145 |
146 | div.innerHTML = svg;
147 |
148 | // proxy svg image
149 | var img = new Image()
150 | img.src = 'data:image/svg+xml;utf8,' + svg
151 |
152 | // write svg image to canvas for pixels
153 | var canvas = document.createElement('canvas');
154 | canvas.width = width;
155 | canvas.height = height;
156 | var context = canvas.getContext('2d');
157 |
158 | context.drawImage(img, 0, 0, width, height);
159 | // var a = document.createElement("a");
160 | // a.download = 'test.png';
161 | // a.href = canvas.toDataURL("image/png");
162 | // a.click()
163 | // z = cc.getImageData(0,0,c.width, c.height)
164 |
165 |
166 | document.body.appendChild(div);
167 | }
--------------------------------------------------------------------------------
/app/transfer-image.js:
--------------------------------------------------------------------------------
1 | var saveTo = document.getElementById('saveTo');
2 | var saveStatus = document.getElementById('status');
3 |
4 | // this file extends the web app with file system + electron capabilities
5 |
6 | if (typeof(global) === 'object') {
7 | // check that we are in electron. then we cheat.
8 | var lastLoad = localStorage.lastLoad;
9 | if (lastLoad) {
10 | var filenames = JSON.parse(lastLoad);
11 | // TODO move this to PhotoList deserializer
12 |
13 | // var now = Date.now();
14 | // filenames.forEach( (filename, i) => {
15 | // console.log('Previously loaded', filename);
16 |
17 | // var img = new Image();
18 | // img.src = 'file://' + filename;
19 | // img.onload = function() {
20 | // console.log('loaded', Date.now() - now);
21 | // processImage(img, filename, i);
22 | // }
23 | // } );
24 |
25 | var fs = require('fs');
26 |
27 | filenames.forEach( (filename, i) => {
28 | var buffer = fs.readFileSync(filename);
29 |
30 | // fs.readFile(filename, function(err, buffer) {;
31 | var now = Date.now();
32 | var img = document.createElement("img");
33 | // img.src = objectURL;
34 |
35 | console.time('exif')
36 | var exif = EXIF.readFromBinaryFile(buffer.buffer);
37 | console.timeEnd('exif')
38 |
39 | img.src = filename;
40 |
41 | img.exifdata = exif;
42 |
43 | img.onload = function() {
44 | console.log('loaded', Date.now() - now);
45 | processImage(img, filename, i);
46 | }
47 | })
48 | // });
49 | }
50 |
51 | function saveImage() {
52 | var out = selectedPhoto.filename.split('/').pop();
53 | // out = `${__dirname}/captures/${out}`;
54 | out = `${saveTo.innerHTML}/${out}`;
55 | saveImageTo(selectedPhoto, currentStyle, out);
56 | }
57 |
58 | function saveAll() {
59 | var items = photos.items();
60 |
61 | var start = Date.now();
62 |
63 | var ok = function() {
64 | var f = items.pop();
65 | if (!f) {
66 | console.log('Batch Done!', (Date.now() - start) / 1000);
67 | saveStatus.innerHTML = 'Done!';
68 | electron.shell.openItem(saveTo.innerHTML);
69 | if (win) win.close();
70 | return;
71 | }
72 |
73 | var out = f.filename.split('/').pop();
74 | // out = `${__dirname}/captures/${out}`;
75 | out = `${saveTo.innerHTML}/${out}`;
76 | saveImageTo(f, currentStyle, out, ok);
77 | }
78 |
79 | ok();
80 | }
81 |
82 | var win;
83 |
84 | var electron = require('electron');
85 | var remote = electron.remote;
86 |
87 | window.onbeforeunload = function() {
88 | console.log('unloading...');
89 | if (win) {
90 | win.close();
91 | win = null;
92 | }
93 |
94 | // remote.ipcMain.removeAllListeners();
95 | }
96 |
97 | function saveImageTo(photo, currentStyle, out, done) {
98 | saveStatus.innerHTML = 'Exporting...';
99 |
100 | var selectedFile = photo.filename;
101 | var currentImage = photo.img;
102 | var orientation = photo.orientation;
103 |
104 | var localScreen = electron.screen
105 | // var display = localScreen.getPrimaryDisplay().workAreaSize;
106 | var start = Date.now();
107 |
108 | var longest = Math.max(currentImage.naturalWidth || 0, currentImage.natualHeight || 0);
109 | if (longside.value) longest = longside.value | 0;
110 |
111 | var width = currentImage.naturalWidth + 500 * 1;
112 | var height = currentImage.naturalHeight + 500 * 1;
113 |
114 | // var shift =
115 | if (orientation > 4) {
116 | var t = width;
117 | width = height;
118 | height = t;
119 | }
120 |
121 | width = longest + 200;
122 | height = longest + 200;
123 |
124 | if (!win) {
125 | win = new remote.BrowserWindow ({
126 | x: 0, // display.width
127 | y: 0, // display.height
128 | width: width,
129 | height: height,
130 | // resizable: false,
131 | 'skip-taskbar': true,
132 | show: false,
133 | 'enable-larger-than-screen': true
134 | });
135 |
136 | win.loadURL(`file://${__dirname}/capture-image.html`);
137 |
138 | console.log('saving image', selectedFile, currentStyle, out);
139 | win.webContents.on('did-finish-load', function() {
140 | console.log('did-finish-load')
141 | // window.webContents.send('ping', 'whoooooooh!');
142 | win.webContents.executeJavaScript(`load('${selectedFile}', '${currentStyle}', '${out}', ${longest}, ${orientation})`);
143 | });
144 | } else {
145 | win.webContents.executeJavaScript(`load('${selectedFile}', '${currentStyle}', '${out}', ${longest}, ${orientation})`);
146 | }
147 |
148 | // function ok (sender, msg) {
149 | // console.log('image transferred', (Date.now() - start) / 1000);
150 |
151 | // remote.ipcMain.removeListener('signal', ok);
152 |
153 | // if (done) done();
154 | // }
155 |
156 | // remote.ipcMain.on('signal', ok);
157 |
158 | win.on('closed', function() {
159 | console.log('closed', (Date.now() - start) / 1000);
160 | win = null;
161 |
162 | if (done) {
163 | done();
164 | }
165 | });
166 |
167 | //
168 | }
169 |
170 |
171 | function outputFolder() {
172 | var dialog = remote.dialog;
173 | var moo = dialog.showOpenDialog(null, {
174 | properties: ['openDirectory', 'createDirectory']
175 | });
176 |
177 | if (moo.length) {
178 | saveTo.innerHTML = moo[0];
179 | localStorage.saveToLocation = moo[0];
180 | }
181 |
182 | console.log(moo);
183 | }
184 | saveTo.innerHTML = localStorage.saveToLocation || __dirname;
185 | }
--------------------------------------------------------------------------------
/app/vendor/exif.js:
--------------------------------------------------------------------------------
1 | (function() {
2 |
3 | var debug = false;
4 |
5 | var root = this;
6 |
7 | var EXIF = function(obj) {
8 | if (obj instanceof EXIF) return obj;
9 | if (!(this instanceof EXIF)) return new EXIF(obj);
10 | this.EXIFwrapped = obj;
11 | };
12 |
13 | if (typeof exports !== 'undefined') {
14 | if (typeof module !== 'undefined' && module.exports) {
15 | exports = module.exports = EXIF;
16 | }
17 | exports.EXIF = EXIF;
18 | } else {
19 | root.EXIF = EXIF;
20 | }
21 |
22 | var ExifTags = EXIF.Tags = {
23 |
24 | // version tags
25 | 0x9000 : "ExifVersion", // EXIF version
26 | 0xA000 : "FlashpixVersion", // Flashpix format version
27 |
28 | // colorspace tags
29 | 0xA001 : "ColorSpace", // Color space information tag
30 |
31 | // image configuration
32 | 0xA002 : "PixelXDimension", // Valid width of meaningful image
33 | 0xA003 : "PixelYDimension", // Valid height of meaningful image
34 | 0x9101 : "ComponentsConfiguration", // Information about channels
35 | 0x9102 : "CompressedBitsPerPixel", // Compressed bits per pixel
36 |
37 | // user information
38 | 0x927C : "MakerNote", // Any desired information written by the manufacturer
39 | 0x9286 : "UserComment", // Comments by user
40 |
41 | // related file
42 | 0xA004 : "RelatedSoundFile", // Name of related sound file
43 |
44 | // date and time
45 | 0x9003 : "DateTimeOriginal", // Date and time when the original image was generated
46 | 0x9004 : "DateTimeDigitized", // Date and time when the image was stored digitally
47 | 0x9290 : "SubsecTime", // Fractions of seconds for DateTime
48 | 0x9291 : "SubsecTimeOriginal", // Fractions of seconds for DateTimeOriginal
49 | 0x9292 : "SubsecTimeDigitized", // Fractions of seconds for DateTimeDigitized
50 |
51 | // picture-taking conditions
52 | 0x829A : "ExposureTime", // Exposure time (in seconds)
53 | 0x829D : "FNumber", // F number
54 | 0x8822 : "ExposureProgram", // Exposure program
55 | 0x8824 : "SpectralSensitivity", // Spectral sensitivity
56 | 0x8827 : "ISOSpeedRatings", // ISO speed rating
57 | 0x8828 : "OECF", // Optoelectric conversion factor
58 | 0x9201 : "ShutterSpeedValue", // Shutter speed
59 | 0x9202 : "ApertureValue", // Lens aperture
60 | 0x9203 : "BrightnessValue", // Value of brightness
61 | 0x9204 : "ExposureBias", // Exposure bias
62 | 0x9205 : "MaxApertureValue", // Smallest F number of lens
63 | 0x9206 : "SubjectDistance", // Distance to subject in meters
64 | 0x9207 : "MeteringMode", // Metering mode
65 | 0x9208 : "LightSource", // Kind of light source
66 | 0x9209 : "Flash", // Flash status
67 | 0x9214 : "SubjectArea", // Location and area of main subject
68 | 0x920A : "FocalLength", // Focal length of the lens in mm
69 | 0xA20B : "FlashEnergy", // Strobe energy in BCPS
70 | 0xA20C : "SpatialFrequencyResponse", //
71 | 0xA20E : "FocalPlaneXResolution", // Number of pixels in width direction per FocalPlaneResolutionUnit
72 | 0xA20F : "FocalPlaneYResolution", // Number of pixels in height direction per FocalPlaneResolutionUnit
73 | 0xA210 : "FocalPlaneResolutionUnit", // Unit for measuring FocalPlaneXResolution and FocalPlaneYResolution
74 | 0xA214 : "SubjectLocation", // Location of subject in image
75 | 0xA215 : "ExposureIndex", // Exposure index selected on camera
76 | 0xA217 : "SensingMethod", // Image sensor type
77 | 0xA300 : "FileSource", // Image source (3 == DSC)
78 | 0xA301 : "SceneType", // Scene type (1 == directly photographed)
79 | 0xA302 : "CFAPattern", // Color filter array geometric pattern
80 | 0xA401 : "CustomRendered", // Special processing
81 | 0xA402 : "ExposureMode", // Exposure mode
82 | 0xA403 : "WhiteBalance", // 1 = auto white balance, 2 = manual
83 | 0xA404 : "DigitalZoomRation", // Digital zoom ratio
84 | 0xA405 : "FocalLengthIn35mmFilm", // Equivalent foacl length assuming 35mm film camera (in mm)
85 | 0xA406 : "SceneCaptureType", // Type of scene
86 | 0xA407 : "GainControl", // Degree of overall image gain adjustment
87 | 0xA408 : "Contrast", // Direction of contrast processing applied by camera
88 | 0xA409 : "Saturation", // Direction of saturation processing applied by camera
89 | 0xA40A : "Sharpness", // Direction of sharpness processing applied by camera
90 | 0xA40B : "DeviceSettingDescription", //
91 | 0xA40C : "SubjectDistanceRange", // Distance to subject
92 |
93 | // other tags
94 | 0xA005 : "InteroperabilityIFDPointer",
95 | 0xA420 : "ImageUniqueID" // Identifier assigned uniquely to each image
96 | };
97 |
98 | var TiffTags = EXIF.TiffTags = {
99 | 0x0100 : "ImageWidth",
100 | 0x0101 : "ImageHeight",
101 | 0x8769 : "ExifIFDPointer",
102 | 0x8825 : "GPSInfoIFDPointer",
103 | 0xA005 : "InteroperabilityIFDPointer",
104 | 0x0102 : "BitsPerSample",
105 | 0x0103 : "Compression",
106 | 0x0106 : "PhotometricInterpretation",
107 | 0x0112 : "Orientation",
108 | 0x0115 : "SamplesPerPixel",
109 | 0x011C : "PlanarConfiguration",
110 | 0x0212 : "YCbCrSubSampling",
111 | 0x0213 : "YCbCrPositioning",
112 | 0x011A : "XResolution",
113 | 0x011B : "YResolution",
114 | 0x0128 : "ResolutionUnit",
115 | 0x0111 : "StripOffsets",
116 | 0x0116 : "RowsPerStrip",
117 | 0x0117 : "StripByteCounts",
118 | 0x0201 : "JPEGInterchangeFormat",
119 | 0x0202 : "JPEGInterchangeFormatLength",
120 | 0x012D : "TransferFunction",
121 | 0x013E : "WhitePoint",
122 | 0x013F : "PrimaryChromaticities",
123 | 0x0211 : "YCbCrCoefficients",
124 | 0x0214 : "ReferenceBlackWhite",
125 | 0x0132 : "DateTime",
126 | 0x010E : "ImageDescription",
127 | 0x010F : "Make",
128 | 0x0110 : "Model",
129 | 0x0131 : "Software",
130 | 0x013B : "Artist",
131 | 0x8298 : "Copyright"
132 | };
133 |
134 | var GPSTags = EXIF.GPSTags = {
135 | 0x0000 : "GPSVersionID",
136 | 0x0001 : "GPSLatitudeRef",
137 | 0x0002 : "GPSLatitude",
138 | 0x0003 : "GPSLongitudeRef",
139 | 0x0004 : "GPSLongitude",
140 | 0x0005 : "GPSAltitudeRef",
141 | 0x0006 : "GPSAltitude",
142 | 0x0007 : "GPSTimeStamp",
143 | 0x0008 : "GPSSatellites",
144 | 0x0009 : "GPSStatus",
145 | 0x000A : "GPSMeasureMode",
146 | 0x000B : "GPSDOP",
147 | 0x000C : "GPSSpeedRef",
148 | 0x000D : "GPSSpeed",
149 | 0x000E : "GPSTrackRef",
150 | 0x000F : "GPSTrack",
151 | 0x0010 : "GPSImgDirectionRef",
152 | 0x0011 : "GPSImgDirection",
153 | 0x0012 : "GPSMapDatum",
154 | 0x0013 : "GPSDestLatitudeRef",
155 | 0x0014 : "GPSDestLatitude",
156 | 0x0015 : "GPSDestLongitudeRef",
157 | 0x0016 : "GPSDestLongitude",
158 | 0x0017 : "GPSDestBearingRef",
159 | 0x0018 : "GPSDestBearing",
160 | 0x0019 : "GPSDestDistanceRef",
161 | 0x001A : "GPSDestDistance",
162 | 0x001B : "GPSProcessingMethod",
163 | 0x001C : "GPSAreaInformation",
164 | 0x001D : "GPSDateStamp",
165 | 0x001E : "GPSDifferential"
166 | };
167 |
168 | // EXIF 2.3 Spec
169 | var IFD1Tags = EXIF.IFD1Tags = {
170 | 0x0100: "ImageWidth",
171 | 0x0101: "ImageHeight",
172 | 0x0102: "BitsPerSample",
173 | 0x0103: "Compression",
174 | 0x0106: "PhotometricInterpretation",
175 | 0x0111: "StripOffsets",
176 | 0x0112: "Orientation",
177 | 0x0115: "SamplesPerPixel",
178 | 0x0116: "RowsPerStrip",
179 | 0x0117: "StripByteCounts",
180 | 0x011A: "XResolution",
181 | 0x011B: "YResolution",
182 | 0x011C: "PlanarConfiguration",
183 | 0x0128: "ResolutionUnit",
184 | 0x0201: "JpegIFOffset", // When image format is JPEG, this value show offset to JPEG data stored.(aka "ThumbnailOffset" or "JPEGInterchangeFormat")
185 | 0x0202: "JpegIFByteCount", // When image format is JPEG, this value shows data size of JPEG image (aka "ThumbnailLength" or "JPEGInterchangeFormatLength")
186 | 0x0211: "YCbCrCoefficients",
187 | 0x0212: "YCbCrSubSampling",
188 | 0x0213: "YCbCrPositioning",
189 | 0x0214: "ReferenceBlackWhite"
190 | };
191 |
192 | var StringValues = EXIF.StringValues = {
193 | ExposureProgram : {
194 | 0 : "Not defined",
195 | 1 : "Manual",
196 | 2 : "Normal program",
197 | 3 : "Aperture priority",
198 | 4 : "Shutter priority",
199 | 5 : "Creative program",
200 | 6 : "Action program",
201 | 7 : "Portrait mode",
202 | 8 : "Landscape mode"
203 | },
204 | MeteringMode : {
205 | 0 : "Unknown",
206 | 1 : "Average",
207 | 2 : "CenterWeightedAverage",
208 | 3 : "Spot",
209 | 4 : "MultiSpot",
210 | 5 : "Pattern",
211 | 6 : "Partial",
212 | 255 : "Other"
213 | },
214 | LightSource : {
215 | 0 : "Unknown",
216 | 1 : "Daylight",
217 | 2 : "Fluorescent",
218 | 3 : "Tungsten (incandescent light)",
219 | 4 : "Flash",
220 | 9 : "Fine weather",
221 | 10 : "Cloudy weather",
222 | 11 : "Shade",
223 | 12 : "Daylight fluorescent (D 5700 - 7100K)",
224 | 13 : "Day white fluorescent (N 4600 - 5400K)",
225 | 14 : "Cool white fluorescent (W 3900 - 4500K)",
226 | 15 : "White fluorescent (WW 3200 - 3700K)",
227 | 17 : "Standard light A",
228 | 18 : "Standard light B",
229 | 19 : "Standard light C",
230 | 20 : "D55",
231 | 21 : "D65",
232 | 22 : "D75",
233 | 23 : "D50",
234 | 24 : "ISO studio tungsten",
235 | 255 : "Other"
236 | },
237 | Flash : {
238 | 0x0000 : "Flash did not fire",
239 | 0x0001 : "Flash fired",
240 | 0x0005 : "Strobe return light not detected",
241 | 0x0007 : "Strobe return light detected",
242 | 0x0009 : "Flash fired, compulsory flash mode",
243 | 0x000D : "Flash fired, compulsory flash mode, return light not detected",
244 | 0x000F : "Flash fired, compulsory flash mode, return light detected",
245 | 0x0010 : "Flash did not fire, compulsory flash mode",
246 | 0x0018 : "Flash did not fire, auto mode",
247 | 0x0019 : "Flash fired, auto mode",
248 | 0x001D : "Flash fired, auto mode, return light not detected",
249 | 0x001F : "Flash fired, auto mode, return light detected",
250 | 0x0020 : "No flash function",
251 | 0x0041 : "Flash fired, red-eye reduction mode",
252 | 0x0045 : "Flash fired, red-eye reduction mode, return light not detected",
253 | 0x0047 : "Flash fired, red-eye reduction mode, return light detected",
254 | 0x0049 : "Flash fired, compulsory flash mode, red-eye reduction mode",
255 | 0x004D : "Flash fired, compulsory flash mode, red-eye reduction mode, return light not detected",
256 | 0x004F : "Flash fired, compulsory flash mode, red-eye reduction mode, return light detected",
257 | 0x0059 : "Flash fired, auto mode, red-eye reduction mode",
258 | 0x005D : "Flash fired, auto mode, return light not detected, red-eye reduction mode",
259 | 0x005F : "Flash fired, auto mode, return light detected, red-eye reduction mode"
260 | },
261 | SensingMethod : {
262 | 1 : "Not defined",
263 | 2 : "One-chip color area sensor",
264 | 3 : "Two-chip color area sensor",
265 | 4 : "Three-chip color area sensor",
266 | 5 : "Color sequential area sensor",
267 | 7 : "Trilinear sensor",
268 | 8 : "Color sequential linear sensor"
269 | },
270 | SceneCaptureType : {
271 | 0 : "Standard",
272 | 1 : "Landscape",
273 | 2 : "Portrait",
274 | 3 : "Night scene"
275 | },
276 | SceneType : {
277 | 1 : "Directly photographed"
278 | },
279 | CustomRendered : {
280 | 0 : "Normal process",
281 | 1 : "Custom process"
282 | },
283 | WhiteBalance : {
284 | 0 : "Auto white balance",
285 | 1 : "Manual white balance"
286 | },
287 | GainControl : {
288 | 0 : "None",
289 | 1 : "Low gain up",
290 | 2 : "High gain up",
291 | 3 : "Low gain down",
292 | 4 : "High gain down"
293 | },
294 | Contrast : {
295 | 0 : "Normal",
296 | 1 : "Soft",
297 | 2 : "Hard"
298 | },
299 | Saturation : {
300 | 0 : "Normal",
301 | 1 : "Low saturation",
302 | 2 : "High saturation"
303 | },
304 | Sharpness : {
305 | 0 : "Normal",
306 | 1 : "Soft",
307 | 2 : "Hard"
308 | },
309 | SubjectDistanceRange : {
310 | 0 : "Unknown",
311 | 1 : "Macro",
312 | 2 : "Close view",
313 | 3 : "Distant view"
314 | },
315 | FileSource : {
316 | 3 : "DSC"
317 | },
318 |
319 | Components : {
320 | 0 : "",
321 | 1 : "Y",
322 | 2 : "Cb",
323 | 3 : "Cr",
324 | 4 : "R",
325 | 5 : "G",
326 | 6 : "B"
327 | }
328 | };
329 |
330 | function addEvent(element, event, handler) {
331 | if (element.addEventListener) {
332 | element.addEventListener(event, handler, false);
333 | } else if (element.attachEvent) {
334 | element.attachEvent("on" + event, handler);
335 | }
336 | }
337 |
338 | function imageHasData(img) {
339 | return !!(img.exifdata);
340 | }
341 |
342 |
343 | function base64ToArrayBuffer(base64, contentType) {
344 | contentType = contentType || base64.match(/^data\:([^\;]+)\;base64,/mi)[1] || ''; // e.g. 'data:image/jpeg;base64,...' => 'image/jpeg'
345 | base64 = base64.replace(/^data\:([^\;]+)\;base64,/gmi, '');
346 | var binary = atob(base64);
347 | var len = binary.length;
348 | var buffer = new ArrayBuffer(len);
349 | var view = new Uint8Array(buffer);
350 | for (var i = 0; i < len; i++) {
351 | view[i] = binary.charCodeAt(i);
352 | }
353 | return buffer;
354 | }
355 |
356 | function objectURLToBlob(url, callback) {
357 | var http = new XMLHttpRequest();
358 | http.open("GET", url, true);
359 | http.responseType = "blob";
360 | http.onload = function(e) {
361 | if (this.status == 200 || this.status === 0) {
362 | callback(this.response);
363 | }
364 | };
365 | http.send();
366 | }
367 |
368 | function getImageData(img, callback) {
369 | function handleBinaryFile(binFile) {
370 | var data = findEXIFinJPEG(binFile);
371 | img.exifdata = data || {};
372 | var iptcdata = findIPTCinJPEG(binFile);
373 | img.iptcdata = iptcdata || {};
374 | if (EXIF.isXmpEnabled) {
375 | var xmpdata= findXMPinJPEG(binFile);
376 | img.xmpdata = xmpdata || {};
377 | }
378 | if (callback) {
379 | callback.call(img);
380 | }
381 | }
382 |
383 | if (img.src) {
384 | if (/^data\:/i.test(img.src)) { // Data URI
385 | var arrayBuffer = base64ToArrayBuffer(img.src);
386 | handleBinaryFile(arrayBuffer);
387 |
388 | } else if (/^blob\:/i.test(img.src)) { // Object URL
389 | var fileReader = new FileReader();
390 | fileReader.onload = function(e) {
391 | handleBinaryFile(e.target.result);
392 | };
393 | objectURLToBlob(img.src, function (blob) {
394 | fileReader.readAsArrayBuffer(blob);
395 | });
396 | } else {
397 | var http = new XMLHttpRequest();
398 | http.onload = function() {
399 | if (this.status == 200 || this.status === 0) {
400 | handleBinaryFile(http.response);
401 | } else {
402 | throw "Could not load image";
403 | }
404 | http = null;
405 | };
406 | http.open("GET", img.src, true);
407 | http.responseType = "arraybuffer";
408 | http.send(null);
409 | }
410 | } else if (self.FileReader && (img instanceof self.Blob || img instanceof self.File)) {
411 | var fileReader = new FileReader();
412 | fileReader.onload = function(e) {
413 | if (debug) console.log("Got file of length " + e.target.result.byteLength);
414 | handleBinaryFile(e.target.result);
415 | };
416 |
417 | fileReader.readAsArrayBuffer(img);
418 | }
419 | }
420 |
421 | function findEXIFinJPEG(file) {
422 | var dataView = new DataView(file);
423 |
424 | if (debug) console.log("Got file of length " + file.byteLength);
425 | if ((dataView.getUint8(0) != 0xFF) || (dataView.getUint8(1) != 0xD8)) {
426 | if (debug) console.log("Not a valid JPEG");
427 | return false; // not a valid jpeg
428 | }
429 |
430 | var offset = 2,
431 | length = file.byteLength,
432 | marker;
433 |
434 | while (offset < length) {
435 | if (dataView.getUint8(offset) != 0xFF) {
436 | if (debug) console.log("Not a valid marker at offset " + offset + ", found: " + dataView.getUint8(offset));
437 | return false; // not a valid marker, something is wrong
438 | }
439 |
440 | marker = dataView.getUint8(offset + 1);
441 | if (debug) console.log(marker);
442 |
443 | // we could implement handling for other markers here,
444 | // but we're only looking for 0xFFE1 for EXIF data
445 |
446 | if (marker == 225) {
447 | if (debug) console.log("Found 0xFFE1 marker");
448 |
449 | return readEXIFData(dataView, offset + 4, dataView.getUint16(offset + 2) - 2);
450 |
451 | // offset += 2 + file.getShortAt(offset+2, true);
452 |
453 | } else {
454 | offset += 2 + dataView.getUint16(offset+2);
455 | }
456 |
457 | }
458 |
459 | }
460 |
461 | function findIPTCinJPEG(file) {
462 | var dataView = new DataView(file);
463 |
464 | if (debug) console.log("Got file of length " + file.byteLength);
465 | if ((dataView.getUint8(0) != 0xFF) || (dataView.getUint8(1) != 0xD8)) {
466 | if (debug) console.log("Not a valid JPEG");
467 | return false; // not a valid jpeg
468 | }
469 |
470 | var offset = 2,
471 | length = file.byteLength;
472 |
473 |
474 | var isFieldSegmentStart = function(dataView, offset){
475 | return (
476 | dataView.getUint8(offset) === 0x38 &&
477 | dataView.getUint8(offset+1) === 0x42 &&
478 | dataView.getUint8(offset+2) === 0x49 &&
479 | dataView.getUint8(offset+3) === 0x4D &&
480 | dataView.getUint8(offset+4) === 0x04 &&
481 | dataView.getUint8(offset+5) === 0x04
482 | );
483 | };
484 |
485 | while (offset < length) {
486 |
487 | if ( isFieldSegmentStart(dataView, offset )){
488 |
489 | // Get the length of the name header (which is padded to an even number of bytes)
490 | var nameHeaderLength = dataView.getUint8(offset+7);
491 | if(nameHeaderLength % 2 !== 0) nameHeaderLength += 1;
492 | // Check for pre photoshop 6 format
493 | if(nameHeaderLength === 0) {
494 | // Always 4
495 | nameHeaderLength = 4;
496 | }
497 |
498 | var startOffset = offset + 8 + nameHeaderLength;
499 | var sectionLength = dataView.getUint16(offset + 6 + nameHeaderLength);
500 |
501 | return readIPTCData(file, startOffset, sectionLength);
502 |
503 | break;
504 |
505 | }
506 |
507 |
508 | // Not the marker, continue searching
509 | offset++;
510 |
511 | }
512 |
513 | }
514 | var IptcFieldMap = {
515 | 0x78 : 'caption',
516 | 0x6E : 'credit',
517 | 0x19 : 'keywords',
518 | 0x37 : 'dateCreated',
519 | 0x50 : 'byline',
520 | 0x55 : 'bylineTitle',
521 | 0x7A : 'captionWriter',
522 | 0x69 : 'headline',
523 | 0x74 : 'copyright',
524 | 0x0F : 'category'
525 | };
526 | function readIPTCData(file, startOffset, sectionLength){
527 | var dataView = new DataView(file);
528 | var data = {};
529 | var fieldValue, fieldName, dataSize, segmentType, segmentSize;
530 | var segmentStartPos = startOffset;
531 | while(segmentStartPos < startOffset+sectionLength) {
532 | if(dataView.getUint8(segmentStartPos) === 0x1C && dataView.getUint8(segmentStartPos+1) === 0x02){
533 | segmentType = dataView.getUint8(segmentStartPos+2);
534 | if(segmentType in IptcFieldMap) {
535 | dataSize = dataView.getInt16(segmentStartPos+3);
536 | segmentSize = dataSize + 5;
537 | fieldName = IptcFieldMap[segmentType];
538 | fieldValue = getStringFromDB(dataView, segmentStartPos+5, dataSize);
539 | // Check if we already stored a value with this name
540 | if(data.hasOwnProperty(fieldName)) {
541 | // Value already stored with this name, create multivalue field
542 | if(data[fieldName] instanceof Array) {
543 | data[fieldName].push(fieldValue);
544 | }
545 | else {
546 | data[fieldName] = [data[fieldName], fieldValue];
547 | }
548 | }
549 | else {
550 | data[fieldName] = fieldValue;
551 | }
552 | }
553 |
554 | }
555 | segmentStartPos++;
556 | }
557 | return data;
558 | }
559 |
560 |
561 |
562 | function readTags(file, tiffStart, dirStart, strings, bigEnd) {
563 | var entries = file.getUint16(dirStart, !bigEnd),
564 | tags = {},
565 | entryOffset, tag,
566 | i;
567 |
568 | for (i=0;i 4 ? valueOffset : (entryOffset + 8);
593 | vals = [];
594 | for (n=0;n 4 ? valueOffset : (entryOffset + 8);
602 | return getStringFromDB(file, offset, numValues-1);
603 |
604 | case 3: // short, 16 bit int
605 | if (numValues == 1) {
606 | return file.getUint16(entryOffset + 8, !bigEnd);
607 | } else {
608 | offset = numValues > 2 ? valueOffset : (entryOffset + 8);
609 | vals = [];
610 | for (n=0;n dataView.byteLength) { // this should not happen
695 | // console.log('******** IFD1Offset is outside the bounds of the DataView ********');
696 | return {};
697 | }
698 | // console.log('******* thumbnail IFD offset (IFD1) is: %s', IFD1OffsetPointer);
699 |
700 | var thumbTags = readTags(dataView, tiffStart, tiffStart + IFD1OffsetPointer, IFD1Tags, bigEnd)
701 |
702 | // EXIF 2.3 specification for JPEG format thumbnail
703 |
704 | // If the value of Compression(0x0103) Tag in IFD1 is '6', thumbnail image format is JPEG.
705 | // Most of Exif image uses JPEG format for thumbnail. In that case, you can get offset of thumbnail
706 | // by JpegIFOffset(0x0201) Tag in IFD1, size of thumbnail by JpegIFByteCount(0x0202) Tag.
707 | // Data format is ordinary JPEG format, starts from 0xFFD8 and ends by 0xFFD9. It seems that
708 | // JPEG format and 160x120pixels of size are recommended thumbnail format for Exif2.1 or later.
709 |
710 | if (thumbTags['Compression']) {
711 | // console.log('Thumbnail image found!');
712 |
713 | switch (thumbTags['Compression']) {
714 | case 6:
715 | // console.log('Thumbnail image format is JPEG');
716 | if (thumbTags.JpegIFOffset && thumbTags.JpegIFByteCount) {
717 | // extract the thumbnail
718 | var tOffset = tiffStart + thumbTags.JpegIFOffset;
719 | var tLength = thumbTags.JpegIFByteCount;
720 | thumbTags['blob'] = new Blob([new Uint8Array(dataView.buffer, tOffset, tLength)], {
721 | type: 'image/jpeg'
722 | });
723 | }
724 | break;
725 |
726 | case 1:
727 | console.log("Thumbnail image format is TIFF, which is not implemented.");
728 | break;
729 | default:
730 | console.log("Unknown thumbnail image format '%s'", thumbTags['Compression']);
731 | }
732 | }
733 | else if (thumbTags['PhotometricInterpretation'] == 2) {
734 | console.log("Thumbnail image format is RGB, which is not implemented.");
735 | }
736 | return thumbTags;
737 | }
738 |
739 | function getStringFromDB(buffer, start, length) {
740 | var outstr = "";
741 | for (n = start; n < start+length; n++) {
742 | outstr += String.fromCharCode(buffer.getUint8(n));
743 | }
744 | return outstr;
745 | }
746 |
747 | function readEXIFData(file, start) {
748 | if (getStringFromDB(file, start, 4) != "Exif") {
749 | if (debug) console.log("Not valid EXIF data! " + getStringFromDB(file, start, 4));
750 | return false;
751 | }
752 |
753 | var bigEnd,
754 | tags, tag,
755 | exifData, gpsData,
756 | tiffOffset = start + 6;
757 |
758 | // test for TIFF validity and endianness
759 | if (file.getUint16(tiffOffset) == 0x4949) {
760 | bigEnd = false;
761 | } else if (file.getUint16(tiffOffset) == 0x4D4D) {
762 | bigEnd = true;
763 | } else {
764 | if (debug) console.log("Not valid TIFF data! (no 0x4949 or 0x4D4D)");
765 | return false;
766 | }
767 |
768 | if (file.getUint16(tiffOffset+2, !bigEnd) != 0x002A) {
769 | if (debug) console.log("Not valid TIFF data! (no 0x002A)");
770 | return false;
771 | }
772 |
773 | var firstIFDOffset = file.getUint32(tiffOffset+4, !bigEnd);
774 |
775 | if (firstIFDOffset < 0x00000008) {
776 | if (debug) console.log("Not valid TIFF data! (First offset less than 8)", file.getUint32(tiffOffset+4, !bigEnd));
777 | return false;
778 | }
779 |
780 | tags = readTags(file, tiffOffset, tiffOffset + firstIFDOffset, TiffTags, bigEnd);
781 |
782 | if (tags.ExifIFDPointer) {
783 | exifData = readTags(file, tiffOffset, tiffOffset + tags.ExifIFDPointer, ExifTags, bigEnd);
784 | for (tag in exifData) {
785 | switch (tag) {
786 | case "LightSource" :
787 | case "Flash" :
788 | case "MeteringMode" :
789 | case "ExposureProgram" :
790 | case "SensingMethod" :
791 | case "SceneCaptureType" :
792 | case "SceneType" :
793 | case "CustomRendered" :
794 | case "WhiteBalance" :
795 | case "GainControl" :
796 | case "Contrast" :
797 | case "Saturation" :
798 | case "Sharpness" :
799 | case "SubjectDistanceRange" :
800 | case "FileSource" :
801 | exifData[tag] = StringValues[tag][exifData[tag]];
802 | break;
803 |
804 | case "ExifVersion" :
805 | case "FlashpixVersion" :
806 | exifData[tag] = String.fromCharCode(exifData[tag][0], exifData[tag][1], exifData[tag][2], exifData[tag][3]);
807 | break;
808 |
809 | case "ComponentsConfiguration" :
810 | exifData[tag] =
811 | StringValues.Components[exifData[tag][0]] +
812 | StringValues.Components[exifData[tag][1]] +
813 | StringValues.Components[exifData[tag][2]] +
814 | StringValues.Components[exifData[tag][3]];
815 | break;
816 | }
817 | tags[tag] = exifData[tag];
818 | }
819 | }
820 |
821 | if (tags.GPSInfoIFDPointer) {
822 | gpsData = readTags(file, tiffOffset, tiffOffset + tags.GPSInfoIFDPointer, GPSTags, bigEnd);
823 | for (tag in gpsData) {
824 | switch (tag) {
825 | case "GPSVersionID" :
826 | gpsData[tag] = gpsData[tag][0] +
827 | "." + gpsData[tag][1] +
828 | "." + gpsData[tag][2] +
829 | "." + gpsData[tag][3];
830 | break;
831 | }
832 | tags[tag] = gpsData[tag];
833 | }
834 | }
835 |
836 | // extract thumbnail
837 | tags['thumbnail'] = readThumbnailImage(file, tiffOffset, firstIFDOffset, bigEnd);
838 |
839 | return tags;
840 | }
841 |
842 | function findXMPinJPEG(file) {
843 |
844 | if (!('DOMParser' in self)) {
845 | // console.warn('XML parsing not supported without DOMParser');
846 | return;
847 | }
848 | var dataView = new DataView(file);
849 |
850 | if (debug) console.log("Got file of length " + file.byteLength);
851 | if ((dataView.getUint8(0) != 0xFF) || (dataView.getUint8(1) != 0xD8)) {
852 | if (debug) console.log("Not a valid JPEG");
853 | return false; // not a valid jpeg
854 | }
855 |
856 | var offset = 2,
857 | length = file.byteLength,
858 | dom = new DOMParser();
859 |
860 | while (offset < (length-4)) {
861 | if (getStringFromDB(dataView, offset, 4) == "http") {
862 | var startOffset = offset - 1;
863 | var sectionLength = dataView.getUint16(offset - 2) - 1;
864 | var xmpString = getStringFromDB(dataView, startOffset, sectionLength)
865 | var xmpEndIndex = xmpString.indexOf('xmpmeta>') + 8;
866 | xmpString = xmpString.substring( xmpString.indexOf( ' 0) {
898 | json['@attributes'] = {};
899 | for (var j = 0; j < xml.attributes.length; j++) {
900 | var attribute = xml.attributes.item(j);
901 | json['@attributes'][attribute.nodeName] = attribute.nodeValue;
902 | }
903 | }
904 | } else if (xml.nodeType == 3) { // text node
905 | return xml.nodeValue;
906 | }
907 |
908 | // deal with children
909 | if (xml.hasChildNodes()) {
910 | for(var i = 0; i < xml.childNodes.length; i++) {
911 | var child = xml.childNodes.item(i);
912 | var nodeName = child.nodeName;
913 | if (json[nodeName] == null) {
914 | json[nodeName] = xml2json(child);
915 | } else {
916 | if (json[nodeName].push == null) {
917 | var old = json[nodeName];
918 | json[nodeName] = [];
919 | json[nodeName].push(old);
920 | }
921 | json[nodeName].push(xml2json(child));
922 | }
923 | }
924 | }
925 |
926 | return json;
927 | }
928 |
929 | function xml2Object(xml) {
930 | try {
931 | var obj = {};
932 | if (xml.children.length > 0) {
933 | for (var i = 0; i < xml.children.length; i++) {
934 | var item = xml.children.item(i);
935 | var attributes = item.attributes;
936 | for(var idx in attributes) {
937 | var itemAtt = attributes[idx];
938 | var dataKey = itemAtt.nodeName;
939 | var dataValue = itemAtt.nodeValue;
940 |
941 | if(dataKey !== undefined) {
942 | obj[dataKey] = dataValue;
943 | }
944 | }
945 | var nodeName = item.nodeName;
946 |
947 | if (typeof (obj[nodeName]) == "undefined") {
948 | obj[nodeName] = xml2json(item);
949 | } else {
950 | if (typeof (obj[nodeName].push) == "undefined") {
951 | var old = obj[nodeName];
952 |
953 | obj[nodeName] = [];
954 | obj[nodeName].push(old);
955 | }
956 | obj[nodeName].push(xml2json(item));
957 | }
958 | }
959 | } else {
960 | obj = xml.textContent;
961 | }
962 | return obj;
963 | } catch (e) {
964 | console.log(e.message);
965 | }
966 | }
967 |
968 | EXIF.enableXmp = function() {
969 | EXIF.isXmpEnabled = true;
970 | }
971 |
972 | EXIF.disableXmp = function() {
973 | EXIF.isXmpEnabled = false;
974 | }
975 |
976 | EXIF.getData = function(img, callback) {
977 | if (((self.Image && img instanceof self.Image)
978 | || (self.HTMLImageElement && img instanceof self.HTMLImageElement))
979 | && !img.complete)
980 | return false;
981 |
982 | if (!imageHasData(img)) {
983 | getImageData(img, callback);
984 | } else {
985 | if (callback) {
986 | callback.call(img);
987 | }
988 | }
989 | return true;
990 | }
991 |
992 | EXIF.getTag = function(img, tag) {
993 | if (!imageHasData(img)) return;
994 | return img.exifdata[tag];
995 | }
996 |
997 | EXIF.getIptcTag = function(img, tag) {
998 | if (!imageHasData(img)) return;
999 | return img.iptcdata[tag];
1000 | }
1001 |
1002 | EXIF.getAllTags = function(img) {
1003 | if (!imageHasData(img)) return {};
1004 | var a,
1005 | data = img.exifdata,
1006 | tags = {};
1007 | for (a in data) {
1008 | if (data.hasOwnProperty(a)) {
1009 | tags[a] = data[a];
1010 | }
1011 | }
1012 | return tags;
1013 | }
1014 |
1015 | EXIF.getAllIptcTags = function(img) {
1016 | if (!imageHasData(img)) return {};
1017 | var a,
1018 | data = img.iptcdata,
1019 | tags = {};
1020 | for (a in data) {
1021 | if (data.hasOwnProperty(a)) {
1022 | tags[a] = data[a];
1023 | }
1024 | }
1025 | return tags;
1026 | }
1027 |
1028 | EXIF.pretty = function(img) {
1029 | if (!imageHasData(img)) return "";
1030 | var a,
1031 | data = img.exifdata,
1032 | strPretty = "";
1033 | for (a in data) {
1034 | if (data.hasOwnProperty(a)) {
1035 | if (typeof data[a] == "object") {
1036 | if (data[a] instanceof Number) {
1037 | strPretty += a + " : " + data[a] + " [" + data[a].numerator + "/" + data[a].denominator + "]\r\n";
1038 | } else {
1039 | strPretty += a + " : [" + data[a].length + " values]\r\n";
1040 | }
1041 | } else {
1042 | strPretty += a + " : " + data[a] + "\r\n";
1043 | }
1044 | }
1045 | }
1046 | return strPretty;
1047 | }
1048 |
1049 | EXIF.readFromBinaryFile = function(file) {
1050 | return findEXIFinJPEG(file);
1051 | }
1052 |
1053 | if (typeof define === 'function' && define.amd) {
1054 | define('exif-js', [], function() {
1055 | return EXIF;
1056 | });
1057 | }
1058 | }.call(this));
1059 |
1060 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "lightbroom",
3 | "version": "1.0.0",
4 | "main": "index.js",
5 | "repository": "git@github.com:zz85/lightbroom.git",
6 | "author": "Joshua Koo ",
7 | "license": "MIT",
8 | "scripts": {
9 | "app": "electron app",
10 | "build": "electron-packager app LightBroom --platform=darwin --arch=x64 --version=0.36.2 --version-string.CompanyName=zz85 --version-string.ProductName=LightBroom"
11 | },
12 | "dependencies": {
13 | "electron-prebuilt": "^0.36.2",
14 | "electron-packager": "^12.0.0"
15 | }
16 | }
17 |
--------------------------------------------------------------------------------