From 0741ad691740581e60c81c932adab9e856b9a7ee Mon Sep 17 00:00:00 2001 From: str Date: Sun, 6 Jan 2019 13:13:16 +0100 Subject: [PATCH] refactor: Extract ExifReader from ExifRenderer. --- .../rendering/DetailPaneRenderer.ts | 4 +- src/electronApp/rendering/ExifRenderer.ts | 101 ------------------ .../rendering/exif/ExifOrientation.ts | 12 +++ src/electronApp/rendering/exif/ExifReader.ts | 46 ++++++++ .../rendering/exif/ExifRenderer.ts | 45 ++++++++ src/electronApp/rendering/exif/ExifTagSet.ts | 56 ++++++++++ 6 files changed, 161 insertions(+), 103 deletions(-) delete mode 100644 src/electronApp/rendering/ExifRenderer.ts create mode 100644 src/electronApp/rendering/exif/ExifOrientation.ts create mode 100644 src/electronApp/rendering/exif/ExifReader.ts create mode 100644 src/electronApp/rendering/exif/ExifRenderer.ts create mode 100644 src/electronApp/rendering/exif/ExifTagSet.ts diff --git a/src/electronApp/rendering/DetailPaneRenderer.ts b/src/electronApp/rendering/DetailPaneRenderer.ts index f87c589..99a0321 100644 --- a/src/electronApp/rendering/DetailPaneRenderer.ts +++ b/src/electronApp/rendering/DetailPaneRenderer.ts @@ -4,7 +4,7 @@ import { ImageDetail } from "../../bars/model/ImageDetail"; import { JQueryUtils } from "../../utils/JQueryUtils"; import { MathUtils } from "../../utils/MathUtils"; import { IOutputter } from "../../utils/outputter/IOutputter"; -import { ExifRenderer } from "./ExifRenderer"; +import { ExifRenderer } from "./exif/ExifRenderer"; import { HistogramRenderer } from "./HistogramRenderer"; import { LoaderRenderer } from "./LoaderRenderer"; @@ -48,7 +48,7 @@ export namespace DetailPaneRenderer { `
file size: ${MathUtils.roundToFewPlaces(image.fileSizeInMb)} Mb
` + `
`; - ExifRenderer.getHtmlForImage(image, "image-exif"); + ExifRenderer.renderHtmlForImage(image, "image-exif"); jquery("#image-text").append(html); } diff --git a/src/electronApp/rendering/ExifRenderer.ts b/src/electronApp/rendering/ExifRenderer.ts deleted file mode 100644 index 854f1bb..0000000 --- a/src/electronApp/rendering/ExifRenderer.ts +++ /dev/null @@ -1,101 +0,0 @@ -import * as fs from "fs"; -import * as path from "path"; -import * as jquery from "jquery"; - -import { ImageDetail } from "../../bars/model/ImageDetail"; - -const jpegDecoder = require("jpg-stream/decoder"); - -export namespace ExifRenderer { - export async function getHtmlForImage(image: ImageDetail, divId: string) { - const html = await getHtmlForImageAsync(image); - - jquery("#" + divId).append(html); - } - - async function getHtmlForImageAsync(image: ImageDetail): Promise { - if(!hasFileExif(image.originalFilepath)) { - return ""; - } - - return new Promise((resolve, reject) => { - // decode a JPEG file to RGB pixels - fs.createReadStream(image.originalFilepath) - .pipe(new jpegDecoder({ width: 600, height: 400 })) - .on("meta", (meta: any) => { - // meta contains an exif object as decoded by - // https://github.com/devongovett/exif-reader - - resolve(parseExif(meta)); - }); - }); - } - - function hasFileExif(filepath: string): boolean { - const extension = path.extname(filepath).toLowerCase(); - - // currently we are decoding only JPEG files. - // note: some PNG files can have some EXIF data but it's not yet standard. - - return [".jpg", ".jpeg"].includes(extension); - } - - function parseExif(meta: any): string { - let html = "
";
-
-        const extract = (holder: any, prop: string): string => {
-            return holder[prop] ? `${[prop]}: ${holder[prop]}\n` : "";
-        };
-
-        const addSection = (title: string, sectionHtml: string) => {
-            if (sectionHtml.length > 0) {
-                html += `--- ${title} ---\n` + sectionHtml;
-            }
-        };
-
-        if (meta.exif) {
-            let imageHtml = "";
-
-            const exif = meta.exif;
-            imageHtml += extract(exif, "ApertureValue");
-            imageHtml += extract(exif, "BrightnessValue");
-            imageHtml += extract(exif, "FNumber");
-            imageHtml += extract(exif, "Flash");
-            imageHtml += extract(exif, "FocalLength");
-            imageHtml += extract(exif, "ISO");
-
-            addSection("Image", imageHtml);
-        }
-
-        if (meta.gps) {
-            let gpsHtml = "";
-
-            const gps = meta.gps;
-            gpsHtml += extract(gps, "GPSAltitude");
-            gpsHtml += extract(gps, "GPSAltitudeRef");
-            gpsHtml += extract(gps, "GPSLatitude");
-            gpsHtml += extract(gps, "GPSLatitudeRef");
-
-            addSection("GPS", gpsHtml);
-        }
-
-        if (meta.image) {
-            let imageHtml = "";
-
-            const gps = meta.image;
-            imageHtml += extract(gps, "ModifyDate");
-            imageHtml += extract(gps, "Orientation");
-            imageHtml += extract(gps, "Software");
-            imageHtml += extract(gps, "XResolution");
-            imageHtml += extract(gps, "YResolution");
-            imageHtml += extract(gps, "Make");
-            imageHtml += extract(gps, "Model");
-
-            addSection("Device", imageHtml);
-        }
-
-        html += "
"; - - return html; - } -} diff --git a/src/electronApp/rendering/exif/ExifOrientation.ts b/src/electronApp/rendering/exif/ExifOrientation.ts new file mode 100644 index 0000000..11f6e38 --- /dev/null +++ b/src/electronApp/rendering/exif/ExifOrientation.ts @@ -0,0 +1,12 @@ +// ref: https://www.daveperrett.com/articles/2012/07/28/exif-orientation-handling-is-a-ghetto/ +// note: rotating *clockwise* +export enum ExifOrientation { + Normal = 0, + Rotated90 = 8, + Rotated180 = 3, + Rotated270 = 6, + ReflectedVerticalAxis = 2, + Rotated90AndReflectedHorizontalAxis = 7, + Rotated180AndReflectedVerticalAxis = 4, + Rotated270AndReflectedHorizontalAxis = 5 +} diff --git a/src/electronApp/rendering/exif/ExifReader.ts b/src/electronApp/rendering/exif/ExifReader.ts new file mode 100644 index 0000000..94d41d3 --- /dev/null +++ b/src/electronApp/rendering/exif/ExifReader.ts @@ -0,0 +1,46 @@ +import * as fs from "fs"; +import * as path from "path"; + +import { ImageDetail } from "../../../bars/model/ImageDetail"; +import { ExifTagSet } from "./ExifTagSet"; + +const jpegDecoder = require("jpg-stream/decoder"); + +export namespace ExifReader { + export async function getExifTagsForImageAsync( + image: ImageDetail + ): Promise { + if (!hasFileExif(image.originalFilepath)) { + return null; + } + + return new Promise((resolve, reject) => { + // decode a JPEG file to RGB pixels + fs.createReadStream(image.originalFilepath) + .pipe(new jpegDecoder({ width: 600, height: 400 })) + .on("meta", (meta: any) => { + // meta contains an exif object as decoded by + // https://github.com/devongovett/exif-reader + + resolve(parseExif(meta)); + }); + }); + } + + function hasFileExif(filepath: string): boolean { + const extension = path.extname(filepath).toLowerCase(); + + // currently we are decoding only JPEG files. + // note: some PNG files can have some EXIF data but it's not yet standard. + + return [".jpg", ".jpeg"].includes(extension); + } + + function parseExif(meta: any): ExifTagSet[] { + return [ + ExifTagSet.fromTags(meta.exif, "Image"), + ExifTagSet.fromTags(meta.image, "Device"), + ExifTagSet.fromTags(meta.gps, "GPS") + ].filter(e => !!e) as ExifTagSet[]; + } +} diff --git a/src/electronApp/rendering/exif/ExifRenderer.ts b/src/electronApp/rendering/exif/ExifRenderer.ts new file mode 100644 index 0000000..f306a56 --- /dev/null +++ b/src/electronApp/rendering/exif/ExifRenderer.ts @@ -0,0 +1,45 @@ +import * as jquery from "jquery"; + +import { ImageDetail } from "../../../bars/model/ImageDetail"; +import { ExifReader } from "./ExifReader"; +import { ExifTagSet } from "./ExifTagSet"; + +export namespace ExifRenderer { + export async function renderHtmlForImage(image: ImageDetail, divId: string) { + const html = await getHtmlForImageAsync(image); + + jquery("#" + divId).append(html); + } + + async function getHtmlForImageAsync(image: ImageDetail): Promise { + const tagSets = await ExifReader.getExifTagsForImageAsync(image); + if (!tagSets) { + return ""; + } + + let html = "
";
+
+        html += tagSets.map(set => renderTagSet(set)).join("\n");
+
+        html += "
"; + + return html; + } + + function renderTagSet(tagSet: ExifTagSet): string { + let sectionHtml = ""; + + tagSet.map.forEach((value, key) => { + if (value !== null && value !== undefined) { + const valueAsText = value.toString(); + sectionHtml += valueAsText.length > 0 ? `${key}: ${valueAsText}\n` : ""; + } + }); + + if (sectionHtml.length > 0) { + return `--- ${tagSet.title} ---\n` + sectionHtml; + } + + return ""; + } +} diff --git a/src/electronApp/rendering/exif/ExifTagSet.ts b/src/electronApp/rendering/exif/ExifTagSet.ts new file mode 100644 index 0000000..2729876 --- /dev/null +++ b/src/electronApp/rendering/exif/ExifTagSet.ts @@ -0,0 +1,56 @@ +export class ExifTagSet { + static fromTags(tags: any, name: string): ExifTagSet | null { + if (!tags) { + return null; + } + + const interestingTags = Object.keys(ExifTag); + + const tagSet = new ExifTagSet(name); + + interestingTags.forEach(tag => { + const value = tags[tag] ? tags[tag] : null; + + if (value) { + tagSet.map.set(tag as ExifTag, value); + } + }); + + return tagSet; + } + + readonly map = new Map(); + + constructor(readonly title: string) {} + + get(tag: ExifTag): string | null { + if (!this.map.has(tag)) { + return null; + } + + return this.map.get(tag)!; + } +} + +enum ExifTag { + // 'exif' + ApertureValue = "ApertureValue", + BrightnessValue = "BrightnessValue", + FNumber = "FNumber", + Flash = "Flash", + FocalLength = "FocalLength", + ISO = "ISO", + // 'gps' + GPSAltitude = "GPSAltitude", + GPSAltitudeRef = "GPSAltitudeRef", + GPSLatitude = "GPSLatitude", + GPSLatitudeRef = "GPSLatitudeRef", + // 'image' + ModifyDate = "ModifyDate", + Orientation = "Orientation", + Software = "Software", + XResolution = "XResolution", + YResolution = "YResolution", + Make = "Make", + Model = "Model" +}