exiftool-vendored

repository·main·Indexed 20 days ago

https://github.com/photostructure/exiftool-vendored.js

A high-performance Node.js wrapper around Phil Harvey's ExifTool for reading, writing, and extracting metadata from images and other files. It provides cross-platform access to metadata, including capabilities to extract thumbnails, previews, and JPEGs from RAW files. The library includes specialized ExifDateTime and ExifTime classes for date handling and timezone inference, as well as configurable performance settings for high-throughput processing.

Tokens
33.8K
Snippets
107
Records
130
Agent score
69%

What's inside exiftool-vendored

  1. What is a Technical Project Plan (TPP)?

    main
    A Technical Project Plan (TPP) is a markdown file located in the _todo/ directory used to persist research, design decisions, and implementation progress across development sessions. It serves as a source of truth for the current state of a feature, preventing loss of context when sessions end. TPPs are human-curated documents that accumulate 'Lore' (non-obvious details and historical context) and should be moved to _done/ once completed.
  2. Design principles for exiftool-vendored

    main

    The project follows Kent Beck's Four Rules of Simple Design to ensure code quality and maintainability. These rules are applied in priority order:

    1. Passes the Tests: Functionality must be proven through automated tests. Tests should assert correct behavior rather than implementation details.
    2. Reveals Intention: Code must clearly express what it does and why through descriptive naming (e.g., parseExifDateTime instead of parseDT) and domain-aligned structure.
    3. No Duplication: Eliminate both obvious and hidden duplication (like parallel hierarchies). The project uses codegen (e.g., mktags.ts) to auto-generate large lookup tables like Tags.ts to avoid manual maintenance.
    4. Fewest Elements: Remove any classes, methods, or abstractions that do not serve the first three rules. Avoid speculative complexity.

    Rule 5 (Implementation Guideline): No bogus guardrails or defaults When assumptions are broken, fail early and visibly:

    • Propagate errors to callers instead of using silent try/catch warnings.
    • Avoid unnecessary existence checks if data is guaranteed to exist; unnecessary guardrails can mislead developers.
    • Never use 'defaults' as a fallback for errors.
  3. Understand the structure of the Tags interface

    main

    The Tags interface is a massive, auto-generated TypeScript interface representing thousands of metadata fields. It is composed of several specialized tag categories that you can access via the object returned by exiftool.read().

    Major categories include:

    • FileTags: File system metadata (size, dates, permissions).
    • EXIFTags: Standard camera settings and technical data (ISO, aperture, etc.).
    • GPSTags: Geolocation information.
    • IPTCTags: Publishing and descriptive metadata.
    • XMPTags: Adobe's extensible metadata platform.
    • MakerNotesTags: Proprietary camera manufacturer data.
    • CompositeTags: Calculated values (e.g., timezone-adjusted dates).
    export interface Tags
      extends
        FileTags,
        EXIFTags,
        GPSTags,
        IPTCTags,
        XMPTags,
        MakerNotesTags,
        CompositeTags,
        ExifToolTags {
      // Combined interface
    }
  4. How to handle malformed UTF-8 bytes

    main

    When ExifTool encounters malformed UTF-8, it marks the bytes with the Unicode replacement character U+FFFD. To recover the original bytes, check the invalidUtf8Bytes sidecar property.

    const tags = await exiftool.read(file);
    // Example of a corrupted string
    // tags.ImageDescription might be "Arch Enemy\rG�teborg, 19.07.2007"
    
    const bytes = tags.invalidUtf8Bytes?.ImageDescription;
    if (bytes instanceof Uint8Array) {
      // Use a specific decoder (e.g., 'macintosh') based on your knowledge of the source
      const recovered = new TextDecoder("macintosh").decode(bytes);
      console.log(recovered); // "Arch Enemy\rGöteborg, 19.07.2007"
    }
    const tags = await exiftool.read(file);
    tags.ImageDescription; // "Arch Enemy\rG�teborg, 19.07.2007"
    
    const bytes = tags.invalidUtf8Bytes?.ImageDescription;
    if (bytes instanceof Uint8Array) {
      // Camera/tag-specific evidence identifies this Kodak value as MacRoman:
      const recovered = new TextDecoder("macintosh").decode(bytes);
      recovered; // "Arch Enemy\rGöteborg, 19.07.2007"
    }
  5. How timezone inference heuristics work

    main

    Because media metadata rarely includes explicit timezones, the library uses three heuristics in priority order to infer the timezone offset:

    1. Explicit Metadata (Highest Priority): Uses explicit timezone tags if present in the file, such as TimeZoneOffset, OffsetTime, OffsetTimeOriginal, or OffsetTimeDigitized.
    2. GPS Location (High Priority): Infers the timezone from GPSLatitude and GPSLongitude coordinates. The library uses tz-lookup by default. Note that coordinates of 0, 0 are considered invalid if the ignoreZeroZeroLatLon option is enabled in ExifToolOptions.
    3. UTC Timestamp Delta (Medium Priority): Calculates the offset by comparing local time (e.g., DateTimeOriginal) with UTC timestamps (e.g., GPSDateTime or DateTimeUTC). Deltas greater than 14 hours are considered invalid.
  6. How to handle missing or undeclared tags defensively

    main

    The Tags interface is a best-effort model of common metadata. When writing code, follow these defensive patterns:

    1. Assume fields are optional: Even common fields like Make or Model might be missing from certain files. Always check for existence before use.
    2. Handle undeclared tags: Rare or vendor-specific tags might exist at runtime but aren't in the TypeScript interface. You can access them by casting the tags object to any or intersecting it with Record<string, unknown>.
    3. Validate value types: ExifTool may return unexpected types for malformed or ambiguous data. Handle cases where a nominally numeric field might return a string.
    const tags = await exiftool.read("photo.jpg");
    
    // Accessing an unlisted/undeclared tag
    const customField = (tags as any).UncommonTag;
  7. Quick Start: Read, Write, and Extract Metadata

    main

    The following example demonstrates the core capabilities of the library: reading metadata, writing new tags, extracting a thumbnail, and properly shutting down the library.

    import { exiftool } from "exiftool-vendored";
    
    // Read metadata
    const tags = await exiftool.read("photo.jpg");
    console.log(`Camera: ${tags.Make} ${tags.Model}`);
    console.log(`Taken: ${tags.DateTimeOriginal}`);
    console.log(`Size: ${tags.ImageWidth}x${tags.ImageHeight}`);
    
    // Write metadata
    await exiftool.write("photo.jpg", {
      XPComment: "Amazing sunset!",
      Copyright: "© 2024 Your Name",
    });
    
    // Extract thumbnail
    await exiftool.extractThumbnail("photo.jpg", "thumb.jpg");
    
    // Always call end() to clean up resources
    await exiftool.end();
    import { exiftool } from "exiftool-vendored";
    
    // Read metadata
    const tags = await exiftool.read("photo.jpg");
    console.log(`Camera: ${tags.Make} ${tags.Model}`);
    console.log(`Taken: ${tags.DateTimeOriginal}`);
    console.log(`Size: ${tags.ImageWidth}x${tags.ImageHeight}`);
    
    // Write metadata
    await exiftool.write("photo.jpg", {
      XPComment: "Amazing sunset!",
      Copyright: "© 2024 Your Name",
    });
    
    // Extract thumbnail
    await exiftool.extractThumbnail("photo.jpg", "thumb.jpg");
    
    await exiftool.end();
  8. Safely work with tag values and missing data

    main

    Most tags are optional because not all files contain the same metadata. When working with tags, you should handle potential undefined values using nullish coalescing or safe checks. Additionally, always check the errors array in the returned object to identify parsing issues.

    const tags = await exiftool.read("photo.jpg");
    
    // 1. Safe checking for existence
    if (tags.Make) {
      console.log(`Camera: ${tags.Make}`);
    }
    
    // 2. Providing defaults
    const make = tags.Make ?? "Unknown";
    const width = tags.ImageWidth ?? 0;
    
    // 3. Fallback chains
    const timestamp = tags.DateTimeOriginal ?? tags.DateTime ?? tags.FileModifyDate;
    
    // 4. Error handling
    if (tags.errors && tags.errors.length > 0) {
      console.warn("Metadata parsing issues:", tags.errors);
    }
  9. Checklist for code reviews in exiftool-vendored

    main

    When reviewing code for this project, use the following checklist based on the project's design philosophy:

    • Tests pass: All functionality is verified via automated tests.
    • Clear intent: Names and structure clearly express the purpose of the code.
    • No duplication: Logic appears in exactly one place.
    • Minimal elements: There is no unused or speculative code.
    • Fail fast: Errors propagate to callers rather than using silent fallbacks or bogus defaults.
  10. Clean up ExifTool resources properly

    main

    To prevent resource leaks, you must ensure ExifTool workers are shut down correctly.

    Manual Cleanup

    For servers or long-running processes, call and await .end() during your application's shutdown procedure (e.g., on SIGINT or SIGTERM).

    import { exiftool } from "exiftool-vendored";
    
    async function shutdown(signal) {
      try {
        await closeApplicationResources();
        await exiftool.end();
      } finally {
        process.kill(process.pid, signal);
      }
    }
    
    process.once("SIGINT", (signal) => void shutdown(signal));
    process.once("SIGTERM", (signal) => void shutdown(signal));

    Automatic Cleanup (TypeScript 5.2+)

    If using TypeScript 5.2+ with explicit resource management, you can use using or await using to bind the instance lifecycle to a scope.

    import { ExifTool } from "exiftool-vendored";
    
    // Starts cleanup when the scope exits, but does not wait for it
    {
      using et = new ExifTool();
      const tags = await et.read("photo.jpg");
    }
    
    // Waits for asynchronous cleanup when the scope exits (recommended)
    {
      await using et = new ExifTool();
      const tags = await et.read("photo.jpg");
    }
    import { exiftool } from "exiftool-vendored";
    
    async function shutdown(signal) {
      try {
        await closeApplicationResources(); // Server, sockets, database, etc.
        await exiftool.end();
      } finally {
        // A signal listener disables Node's default termination behavior. Re-send
        // the signal after cleanup so the process terminates normally.
        process.kill(process.pid, signal);
      }
    }
    
    process.once("SIGINT", (signal) => void shutdown(signal));
    process.once("SIGTERM", (signal) => void shutdown(signal));
  11. Quick Start with exiftool-vendored

    main

    You can use the library in two ways: using the pre-configured singleton exiftool for simple tasks, or creating a custom ExifTool instance for fine-grained control over processes and timeouts.

    import { ExifTool, Settings, exiftool } from "exiftool-vendored";
    
    // 1. Use the singleton for simple cases
    const tags = await exiftool.read("photo.jpg");
    
    // 2. Or create a custom instance
    const et = new ExifTool({
      maxProcs: 4,
      taskTimeoutMillis: 30000,
    });