Skip to content
Snippets Groups Projects
Verified Commit d8c947a3 authored by Janne Mareike Koschinski's avatar Janne Mareike Koschinski
Browse files

Implement basic EXIF support

parent fc474b14
Branches
No related tags found
1 merge request!1Replace entire project structure
Pipeline #2578 failed
Showing
with 548 additions and 12 deletions
......@@ -26,6 +26,11 @@ func GetImage(env environment.FrontendEnvironment) http.Handler {
http.Error(writer, err.Error(), http.StatusInternalServerError)
return
}
image.Metadata, err = env.Repositories.ImageMetadata.List(image)
if err != nil {
http.Error(writer, err.Error(), http.StatusInternalServerError)
return
}
util.ReturnJson(writer, image)
})
......
......@@ -20,6 +20,11 @@ func ListImages(env environment.FrontendEnvironment) http.Handler {
http.Error(writer, err.Error(), http.StatusInternalServerError)
return
}
image.Metadata, err = env.Repositories.ImageMetadata.List(image)
if err != nil {
http.Error(writer, err.Error(), http.StatusInternalServerError)
return
}
images[idx] = image
}
......
# EXIF metadata format
* `Make`
Manufacturer
* `Model`
Device
* `DateTime`
* `DateTimeDigitized`
* `DateTimeOriginal`
* `DigitalZoomRatio`
Zoom
- 0 unused
* `ExposureBiasValue`
Exposure
-100.0 to +100.0
* `ExposureMode`
Exposure Mode
- 0 = auto
- 1 = manual
- 2 = bracket
* `ExposureProgram`
Exposure Program
- 0 undefined
- 1 manual
- 2 normal
- 3 aperture priority
- 4 shutter priority
- 5 creative (depth of field priority)
- 6 action (fast shutter priority)
- 7 portrait (object separation priority)
- 8 landscape (background in focus priority)
* `ExposureTime`
Shutter in seconds
* `FNumber`
Aperture
* `Flash`
Bitfield of Flash metadata (bits counted from LSB to MSB)
- bit 0: flashFired
- bit 1: flashStrobeDetectionAvailable
- bit 2: flashStrobeDetected
- bit 3-4
- 0 undefined
- 1 always on
- 2 always off
- 3 auto
- bit 5: flashAvailable
- bit 6: redEyeReductionAvailable
* `FlashEnergy`
Strobe energy in BCPS
* `FocalLength`
Focal Length in mm
* `FocalLengthIn35mmFilm`
Focal Length in mm compared to a 35mm Film equivalent
* `ISOSpeedRatings`
ISO exposure/speed rating
* `LightSource`
Type of lightsource for white balance
- 0 undefined
- 1 daylight
- 2 fluorescent
- 3 tungsten / incandescent
- 4 flash
- 9 fine weather
- 10 cloudy weather
- 11 shade
- 12 daylight fluorescent 5700-7100K
- 13 day white fluorescent 4600-5400K
- 14 cool white fluorescent 3900-4500K
- 15 white fluorescent 3200-3700K
- 17 standard light A
- 18 standard light B
- 19 standard light C
- 20 D55
- 21 D65
- 22 D75
- 23 D50
- 24 ISO studio tungsten
- 255 other
* `MeteringMode`
exposure metering mode
- 0 undefined
- 1 average
- 2 center weighted average
- 3 spot
- 4 multispot
- 5 pattern
- 6 partial
- 255 other
* `WhiteBalance`
White balance
- 0 auto
- 1 manual
* `SceneCaptureType`
Scene Mode
- 0 standard
- 1 landscape
- 2 portrait
- 3 night scene
* `Contrast` Contrast Processing
- 0 normal
- 1 soft
- 2 hard
* `Sharpness`
Sharpness Processing
- 0 normal
- 1 soft
- 2 hard
* `SubjectDistance`
Distance in meters
* `SubjectDistanceRange`
Distance Type
- 0 unknown
- 1 macro
- 2 close
- 3 distant
* `Software`
Application and version used to generate the image
* `Copyright`
copyright information
......@@ -17,6 +17,7 @@ type Image struct {
OriginalName string `json:"original_name" db:"original_name"`
MimeType string `json:"mime_type" db:"type"`
State string `json:"state" db:"state"`
Metadata map[string]string `json:"metadata,omitempty"`
Url string `json:"url"`
}
......
......@@ -75,12 +75,43 @@ func (processor *ImageProcessor) ProcessTask(ctx context.Context, task *asynq.Ta
return
}
supportedMetadata := map[string]bool{
"Make": true,
"Model": true,
"DateTime": true,
"DateTimeDigitized": true,
"DateTimeOriginal": true,
"DigitalZoomRatio": true,
"ExposureBiasValue": true,
"ExposureMode": true,
"ExposureProgram": true,
"ExposureTime": true,
"FNumber": true,
"Flash": true,
"FlashEnergy": true,
"FocalLength": true,
"FocalLengthIn35mmFilm": true,
"ISOSpeedRatings": true,
"LightSource": true,
"MeteringMode": true,
"WhiteBalance": true,
"Contrast": true,
"Sharpness": true,
"SubjectDistance": true,
"SubjectDistanceRange": true,
"Software": true,
"Copyright": true,
}
metadata := make(map[string]string)
for _, key := range wand.GetImageProperties("exif:*") {
if strings.HasPrefix(key, "exif:thumbnail:") {
continue
}
metadata[strings.TrimPrefix(key, "exif:")] = wand.GetImageProperty(key)
trimmedKey := strings.TrimPrefix(key, "exif:")
if !supportedMetadata[trimmedKey] {
continue
}
metadata[trimmedKey] = wand.GetImageProperty(key)
}
err = processor.env.Repositories.ImageMetadata.Update(payload.ImageId, metadata)
if err != nil {
......
......@@ -8,5 +8,8 @@ export interface Image {
original_name: string,
mime_type: string,
state: string,
metadata: {
[key: string]: string
},
url: string,
}
import {Image} from "../api/model/Image";
import React, {useState} from "react";
import React, {useMemo, useState} from "react";
import {useUpdateImage} from "../api/useUpdateImage";
import {useDeleteImage} from "../api/useDeleteImage";
import {parseMetadata, ratioToTime} from "../metadata/ImageMetadata";
import {ratioToFloat} from "../metadata/Ratio";
import {ExposureMode} from "../metadata/ExposureMode";
import {ExposureProgram} from "../metadata/ExposureProgram";
import {LightSource} from "../metadata/LightSource";
import {MeteringMode} from "../metadata/MeteringMode";
import {WhiteBalance} from "../metadata/WhiteBalance";
import {SceneMode} from "../metadata/SceneMode";
import {ContrastProcessing} from "../metadata/ContrastProcessing";
import {SharpnessProcessing} from "../metadata/SharpnessProcessing";
import {SubjectDistanceRange} from "../metadata/SubjectDistanceRange";
export interface ImageProps {
image: Image
......@@ -13,6 +24,10 @@ export default function ImageView({image}: ImageProps) {
const [title, setTitle] = useState<string>(image.title);
const [description, setDescription] = useState<string>(image.description);
const metadata = useMemo(() =>
parseMetadata(image.metadata),
[image]);
return (
<div>
<p>UpdateError: {JSON.stringify(updateError, null, 2)}</p>
......@@ -45,6 +60,40 @@ export default function ImageView({image}: ImageProps) {
<p>{image.created_at}</p>
<p>{image.updated_at}</p>
<p>{image.state}</p>
<h3>Metadata</h3>
<p><b>Make</b>: {metadata.make}</p>
<p><b>Model</b>: {metadata.model}</p>
<p><b>Software</b>: {metadata.software}</p>
<p><b>Copyright</b>: {metadata.copyright}</p>
<p><b>DateTime Created</b>: {metadata.dateTimeCreated?.toISOString()}</p>
<p><b>DateTime Digitized</b>: {metadata.dateTimeDigitized?.toISOString()}</p>
<p><b>DateTime Original</b>: {metadata.dateTimeOriginal?.toISOString()}</p>
<p><b>Digital Zoom</b>: {ratioToFloat(metadata.digitalZoomRatio)}</p>
<p><b>Exposure</b>: {ratioToFloat(metadata.exposure)}</p>
<p><b>Exposure Mode</b>: {metadata.exposureMode !== undefined ?
ExposureMode[metadata.exposureMode] : "null"}</p>
<p><b>Exposure Program</b>: {metadata.exposureProgram !== undefined ?
ExposureProgram[metadata.exposureProgram] : "null"}</p>
<p><b>Exposure Time</b>: {ratioToTime(metadata.exposureTime)}</p>
<p><b>Aperture</b>: {ratioToFloat(metadata.aperture)}</p>
<p><b>Focal Length</b>: {ratioToFloat(metadata.focalLength)}</p>
<p><b>Focal Length (35mm equivalent)</b>: {ratioToFloat(metadata.focalLength35mm)}</p>
<p><b>ISO</b>: {metadata.isoSpeedRating}</p>
<p><b>Light source</b>: {metadata.lightSource !== undefined ?
LightSource[metadata.lightSource] : "null"}</p>
<p><b>Metering mode</b>: {metadata.meteringMode !== undefined ?
MeteringMode[metadata.meteringMode] : "null"}</p>
<p><b>White balance</b>: {metadata.whiteBalance !== undefined ?
WhiteBalance[metadata.whiteBalance] : "null"}</p>
<p><b>Scene Mode</b>: {metadata.sceneMode !== undefined ?
SceneMode[metadata.sceneMode] : "null"}</p>
<p><b>Contrast Processing</b>: {metadata.contrast !== undefined ?
ContrastProcessing[metadata.contrast] : "null"}</p>
<p><b>Sharpness Processing</b>: {metadata.sharpness !== undefined ?
SharpnessProcessing[metadata.sharpness] : "null"}</p>
<p><b>Subject Distance</b>: {metadata.subjectDistance}</p>
<p><b>Subject Distance Range</b>: {metadata.subjectDistanceRange !== undefined ?
SubjectDistanceRange[metadata.subjectDistanceRange] : "null"}</p>
<img src={image.url + "t"} alt=""/>
<br/>
<input
......
export enum ContrastProcessing {
NORMAL = 0,
SOFT = 1,
HARD = 2,
}
export enum ExposureMode {
AUTO = 0,
MANUAL = 1,
BRACKET = 2,
}
export enum ExposureProgram {
MANUAL = 1,
NORMAL = 2,
APERTURE_PRIORITY = 3,
SHUTTER_PRIORITY = 4,
CREATIVE = 5,
ACTION = 6,
PORTRAIT = 7,
LANDSCAPE = 8,
}
import {FlashMode} from "./FlashMode";
export interface Flash {
available: boolean,
fired: boolean,
strobeDetection: {
available: boolean,
detected: boolean,
},
mode: FlashMode | null,
redEyeReduction: boolean,
}
export enum FlashMode {
ALWAYS_ON = 1,
ALWAYS_OFF = 2,
AUTO = 3
}
import {parseRatio, Ratio} from "./Ratio";
import {Flash} from "./Flash";
import {ExposureMode} from "./ExposureMode";
import {ExposureProgram} from "./ExposureProgram";
import {LightSource} from "./LightSource";
import {MeteringMode} from "./MeteringMode";
import {WhiteBalance} from "./WhiteBalance";
import {SceneMode} from "./SceneMode";
import {ContrastProcessing} from "./ContrastProcessing";
import {SharpnessProcessing} from "./SharpnessProcessing";
import {SubjectDistanceRange} from "./SubjectDistanceRange";
export interface ImageMetadata {
make?: string,
model?: string,
software?: string,
copyright?: string,
dateTimeCreated?: Date,
dateTimeDigitized?: Date,
dateTimeOriginal?: Date,
digitalZoomRatio?: Ratio,
exposure?: Ratio,
exposureMode?: ExposureMode,
exposureProgram?: ExposureProgram,
exposureTime?: Ratio,
aperture?: Ratio,
flash?: Flash,
focalLength?: Ratio,
focalLength35mm?: Ratio,
isoSpeedRating?: number,
lightSource?: LightSource,
meteringMode?: MeteringMode,
whiteBalance?: WhiteBalance,
sceneMode?: SceneMode,
contrast?: ContrastProcessing,
sharpness?: SharpnessProcessing,
subjectDistance?: number,
subjectDistanceRange?: SubjectDistanceRange,
}
export function parseMetadata(metadata: { [key: string]: string }): ImageMetadata {
return {
make: metadata["Make"],
model: metadata["Model"],
software: metadata["Software"],
copyright: metadata["Copyright"],
dateTimeCreated: parseDate(metadata["DateTime"]),
dateTimeDigitized: parseDate(metadata["DateTimeDigitized"]),
dateTimeOriginal: parseDate(metadata["DateTimeOriginal"]),
digitalZoomRatio: parseRatio(metadata["DigitalZoomRatio"]),
exposure: parseRatio(metadata["ExposureBiasValue"]),
exposureMode: parseExposureMode(metadata["ExposureMode"]),
exposureProgram: parseExposureProgram(metadata["ExposureProgram"]),
exposureTime: parseRatio(metadata["ExposureTime"]),
aperture: parseRatio(metadata["FNumber"]),
flash: undefined,
focalLength: parseRatio(metadata["FocalLength"]),
focalLength35mm: parseRatio(metadata["FocalLengthIn35mmFilm"]),
isoSpeedRating: parseNumber(metadata["ISOSpeedRatings"]),
lightSource: parseLightSource(metadata["LightSource"]),
meteringMode: parseMeteringMode(metadata["MeteringMode"]),
whiteBalance: parseWhiteBalance(metadata["WhiteBalance"]),
sceneMode: parseSceneMode(metadata["SceneMode"]),
contrast: parseContrastProcessing(metadata["Contrast"]),
sharpness: parseSharpnessProcessing(metadata["Sharpness"]),
subjectDistance: parseNumber(metadata["SubjectDistance"]),
subjectDistanceRange: parseSubjectDistanceRange(metadata["SubjectDistanceRange"]),
}
}
export function parseDate(value: string): Date | undefined {
const split = value.split(" ");
if (split.length !== 2) {
return undefined;
}
const [date, time] = split;
try {
const parsed = new Date(
date.replaceAll(":", "-") + " " + time
);
parsed.toISOString();
return parsed;
} catch (e) {
return undefined;
}
}
export function parseNumber(value: string): number | undefined {
const number = parseInt(value);
if (isNaN(number) || number === Infinity || number === -Infinity) {
return undefined;
}
return number;
}
export function parseExposureMode(value: string): ExposureMode | undefined {
const numericValue = parseNumber(value)
if (numericValue === undefined) {
return undefined;
}
if (numericValue in Object.values(ExposureMode)) {
return numericValue as ExposureMode;
}
return undefined;
}
export function parseExposureProgram(value: string): ExposureProgram | undefined {
const numericValue = parseNumber(value);
if (numericValue === undefined) {
return undefined;
}
if (numericValue in Object.values(ExposureProgram)) {
return numericValue as ExposureProgram;
}
return undefined;
}
export function parseLightSource(value: string): LightSource | undefined {
const numericValue = parseNumber(value);
if (numericValue === undefined) {
return undefined;
}
if (numericValue in Object.values(LightSource)) {
return numericValue as LightSource;
}
return undefined;
}
export function parseMeteringMode(value: string): MeteringMode | undefined {
const numericValue = parseNumber(value);
if (numericValue === undefined) {
return undefined;
}
if (numericValue in Object.values(MeteringMode)) {
return numericValue as MeteringMode;
}
return undefined;
}
export function parseWhiteBalance(value: string): WhiteBalance | undefined {
const numericValue = parseNumber(value);
if (numericValue === undefined) {
return undefined;
}
if (numericValue in Object.values(WhiteBalance)) {
return numericValue as WhiteBalance;
}
return undefined;
}
export function parseSceneMode(value: string): SceneMode | undefined {
const numericValue = parseNumber(value);
if (numericValue === undefined) {
return undefined;
}
if (numericValue in Object.values(SceneMode)) {
return numericValue as SceneMode;
}
return undefined;
}
export function parseContrastProcessing(value: string): ContrastProcessing | undefined {
const numericValue = parseNumber(value);
if (numericValue === undefined) {
return undefined;
}
if (numericValue in Object.values(ContrastProcessing)) {
return numericValue as ContrastProcessing;
}
return undefined;
}
export function parseSharpnessProcessing(value: string): SharpnessProcessing | undefined {
const numericValue = parseNumber(value);
if (numericValue === undefined) {
return undefined;
}
if (numericValue in Object.values(SharpnessProcessing)) {
return numericValue as SharpnessProcessing;
}
return undefined;
}
export function parseSubjectDistanceRange(value: string): SubjectDistanceRange | undefined {
const numericValue = parseNumber(value);
if (numericValue === undefined) {
return undefined;
}
if (numericValue in Object.values(SubjectDistanceRange)) {
return numericValue as SubjectDistanceRange;
}
return undefined;
}
export function ratioToTime(value: Ratio | undefined): string | undefined {
if (value === undefined) {
return undefined;
}
if (value.numerator > value.denominator) {
return (value.numerator / value.denominator).toFixed(0) + "s";
} else {
return "1/" + (value.denominator / value.numerator) + "s";
}
}
export enum LightSource {
DAYLIGHT = 1,
FLUORESCENT = 2,
INCANDESCENT = 3,
FLASH = 4,
FINE_WEATHER = 9,
CLOUDY_WEATHER = 10,
SHADE = 11,
FLUORESCENT_6400K = 12,
FLUORESCENT_5000K = 13,
FLUORESCENT_4200K = 14,
FLUORESCENT_3450K = 15,
STANDARD_LIGHT_A = 17,
STANDARD_LIGHT_B = 18,
STANDARD_LIGHT_C = 10,
D55 = 20,
D65 = 21,
D75 = 22,
D50 = 23,
ISO_STUDIO_INCANDESCENT = 24,
}
export enum MeteringMode {
AVERAGE = 1,
CENTER_WEIGHTED_AVERAGE = 2,
SPOT = 3,
MULTI_SPOT = 4,
PATTERN = 5,
PARTIAL = 6,
}
export interface Ratio {
numerator: number,
denominator: number
}
export function parseRatio(value: string): Ratio | undefined {
const splitValues = value.split("/");
if (splitValues.length < 1) {
return undefined;
}
const numerator = parseInt(splitValues[0]);
if (isNaN(numerator)) {
return undefined;
}
let denominator;
if (splitValues.length === 1) {
denominator = 1;
} else {
denominator = parseInt(splitValues[1]);
if (isNaN(denominator) || denominator === 0) {
return undefined;
}
}
return {numerator, denominator};
}
export function ratioToFloat(ratio: Ratio | undefined): number | undefined {
if (ratio === undefined) {
return undefined;
}
return ratio.numerator / ratio.denominator;
}
export enum SceneMode {
STANDARD = 0,
LANDSCAPE = 1,
PORTRAIT = 2,
NIGHT_SCENE = 3,
}
export enum SharpnessProcessing {
NORMAL = 0,
SOFT = 1,
HARD = 2,
}
export enum SubjectDistanceRange {
MACRO = 1,
CLOSE = 2,
DISTANT = 3,
}
export enum WhiteBalance {
AUTO = 0,
MANUAL = 1,
}
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment