Read GPSAltitude without its reference tag; keep unnamed EXIF tags
check / check (push) Successful in 1m25s
check / check (push) Successful in 1m25s
exif() takes the altitude from the GPSAltitude tag and negates it only when GPSAltitudeRef is 1, so a file with GPSAltitude and no GPSAltitudeRef keeps its altitude, above sea level. A test covers that file. backup-metadata --exif keeps the EXIF tags exifreader has no name for. The comments in src/exif.ts say the thumbnail's tags are not kept and that a tag with several values computes to an array. Model: opus-5-5
This commit is contained in:
+14
-4
@@ -1,7 +1,7 @@
|
|||||||
// EXIF in an original's bytes, read with exifreader, which reads it from JPEG,
|
// EXIF in an original's bytes, read with exifreader, which reads it from JPEG,
|
||||||
// HEIC/HEIF, AVIF, PNG, WebP and the other image formats it supports.
|
// HEIC/HEIF, AVIF, PNG, WebP and the other image formats it supports.
|
||||||
// `backup-metadata --exif` records every EXIF tag it finds; `Photo.exif()`
|
// `backup-metadata --exif` records every EXIF tag it finds except the
|
||||||
// returns the common fields picked from them here.
|
// thumbnail's; `Photo.exif()` returns the common fields picked from them here.
|
||||||
|
|
||||||
import ExifReader, { type ExpandedTags } from "exifreader";
|
import ExifReader, { type ExpandedTags } from "exifreader";
|
||||||
|
|
||||||
@@ -9,7 +9,11 @@ import ExifReader, { type ExpandedTags } from "exifreader";
|
|||||||
// them (`gps`), and where the EXIF block lies in `bytes` (`metadataRange`).
|
// them (`gps`), and where the EXIF block lies in `bytes` (`metadataRange`).
|
||||||
// Undefined when exifreader cannot read the file at all, such as a video. An
|
// Undefined when exifreader cannot read the file at all, such as a video. An
|
||||||
// EXIF block it finds but reads no tag from comes back as an empty `exif`.
|
// EXIF block it finds but reads no tag from comes back as an empty `exif`.
|
||||||
// Each tag's `computed` holds its value as a plain string or number.
|
// `exif` holds every tag except the thumbnail's; a tag exifreader has no name
|
||||||
|
// for is keyed `undefined-<tag number>`. Each tag's `computed` holds its value
|
||||||
|
// as a string or number, or as an array of them for a tag with several values,
|
||||||
|
// such as `GPSLatitude`'s `[40, 26, 46]`. A fraction with a zero denominator
|
||||||
|
// computes to null.
|
||||||
export const readExifTags = (bytes: Uint8Array): ExpandedTags | undefined => {
|
export const readExifTags = (bytes: Uint8Array): ExpandedTags | undefined => {
|
||||||
try {
|
try {
|
||||||
return ExifReader.loadView(
|
return ExifReader.loadView(
|
||||||
@@ -18,6 +22,7 @@ export const readExifTags = (bytes: Uint8Array): ExpandedTags | undefined => {
|
|||||||
expanded: true,
|
expanded: true,
|
||||||
computed: true,
|
computed: true,
|
||||||
includeOffsets: true,
|
includeOffsets: true,
|
||||||
|
includeUnknown: true,
|
||||||
includeTags: { exif: true, gps: true },
|
includeTags: { exif: true, gps: true },
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
@@ -77,6 +82,7 @@ export const readPhotoExif = (bytes: Uint8Array): PhotoExif => {
|
|||||||
const tags = readExifTags(bytes);
|
const tags = readExifTags(bytes);
|
||||||
const exif = tags?.exif;
|
const exif = tags?.exif;
|
||||||
const gps = tags?.gps;
|
const gps = tags?.gps;
|
||||||
|
const altitude = asNumber(exif?.GPSAltitude?.computed);
|
||||||
const fields: PhotoExif = {
|
const fields: PhotoExif = {
|
||||||
make: asString(exif?.Make?.computed),
|
make: asString(exif?.Make?.computed),
|
||||||
model: asString(exif?.Model?.computed),
|
model: asString(exif?.Model?.computed),
|
||||||
@@ -91,7 +97,11 @@ export const readPhotoExif = (bytes: Uint8Array): PhotoExif => {
|
|||||||
orientation: asNumber(exif?.Orientation?.computed),
|
orientation: asNumber(exif?.Orientation?.computed),
|
||||||
gpsLatitude: asNumber(gps?.Latitude),
|
gpsLatitude: asNumber(gps?.Latitude),
|
||||||
gpsLongitude: asNumber(gps?.Longitude),
|
gpsLongitude: asNumber(gps?.Longitude),
|
||||||
gpsAltitude: asNumber(gps?.Altitude),
|
// A GPSAltitudeRef of 1 means the altitude is below sea level.
|
||||||
|
gpsAltitude:
|
||||||
|
altitude !== undefined && exif?.GPSAltitudeRef?.value === 1
|
||||||
|
? -altitude
|
||||||
|
: altitude,
|
||||||
};
|
};
|
||||||
// Leave out what the file lacks, so a missing field is absent rather than
|
// Leave out what the file lacks, so a missing field is absent rather than
|
||||||
// present and undefined.
|
// present and undefined.
|
||||||
|
|||||||
@@ -46,6 +46,23 @@ const TIFF_UNSET_DATE = [
|
|||||||
...new TextEncoder().encode("0000:00:00 00:00:00\0"), // at 56
|
...new TextEncoder().encode("0000:00:00 00:00:00\0"), // at 56
|
||||||
];
|
];
|
||||||
|
|
||||||
|
// A big-endian TIFF block holding a GPSAltitude of 12.5 m and no
|
||||||
|
// GPSAltitudeRef.
|
||||||
|
const TIFF_ALTITUDE_WITHOUT_REF = [
|
||||||
|
...[0x4d, 0x4d, 0x00, 0x2a, 0x00, 0x00, 0x00, 0x08], // the first IFD at 8
|
||||||
|
// The first IFD, at 8: one entry, then no next IFD.
|
||||||
|
...[0x00, 0x01],
|
||||||
|
// The GPS IFD's offset (0x8825), LONG, 26.
|
||||||
|
...[0x88, 0x25, 0x00, 0x04, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x1a],
|
||||||
|
...[0x00, 0x00, 0x00, 0x00],
|
||||||
|
// The GPS IFD, at 26: one entry, then no next IFD.
|
||||||
|
...[0x00, 0x01],
|
||||||
|
// GPSAltitude (0x0006), one RATIONAL at 44.
|
||||||
|
...[0x00, 0x06, 0x00, 0x05, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x2c],
|
||||||
|
...[0x00, 0x00, 0x00, 0x00],
|
||||||
|
...[0x00, 0x00, 0x00, 0x19, 0x00, 0x00, 0x00, 0x02], // 25/2, at 44
|
||||||
|
];
|
||||||
|
|
||||||
// An APP1 segment whose length field matches its data.
|
// An APP1 segment whose length field matches its data.
|
||||||
const app1 = (data: number[]): number[] => {
|
const app1 = (data: number[]): number[] => {
|
||||||
const len = data.length + 2;
|
const len = data.length + 2;
|
||||||
@@ -70,6 +87,13 @@ describe("readPhotoExif", () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("reads a GPSAltitude without GPSAltitudeRef as above sea level", () => {
|
||||||
|
const data = [...EXIF_HEADER, ...TIFF_ALTITUDE_WITHOUT_REF];
|
||||||
|
expect(readPhotoExif(bytes(SOI, app1(data), SOS))).toStrictEqual({
|
||||||
|
gpsAltitude: 12.5,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
it.each([
|
it.each([
|
||||||
[
|
[
|
||||||
"a file that is not an image",
|
"a file that is not an image",
|
||||||
|
|||||||
Reference in New Issue
Block a user