How Immich Processes and Extracts Metadata (EXIF and OCR) Using metadata.service.ts and ocr.service.ts
Immich extracts EXIF metadata and OCR text through two dedicated NestJS services—MetadataService and OcrService—which process background jobs from the QueueName.MetadataExtraction and QueueName.Ocr queues, persisting structured data to PostgreSQL for search and organization.
Immich processes and extracts metadata from uploaded assets using a dual-pipeline architecture that handles EXIF camera data and searchable OCR text. The system relies on MetadataService in server/src/services/metadata.service.ts for EXIF extraction, motion-photo handling, and face detection, while OcrService in server/src/services/ocr.service.ts manages optical character recognition via machine learning models. Both services operate as background job workers that transform raw files into structured, queryable database records according to the immich-app/immich source code.
EXIF Metadata Extraction Pipeline
The EXIF extraction pipeline runs through MetadataService.handleMetadataExtraction, which processes individual assets queued in QueueName.MetadataExtraction.
Job Orchestration and Batching
The pipeline initiates via handleQueueMetadataExtraction, which streams assets requiring processing and creates batch jobs using JOBS_ASSET_PAGINATION_SIZE (defined in server/src/constants.ts). This queue-all job (JobName.AssetExtractMetadataQueueAll) feeds individual JobName.AssetExtractMetadata tasks to workers.
// MetadataService queues metadata extraction jobs in batches
await jobRepository.queue({
name: JobName.AssetExtractMetadata,
data: { id: assetId },
});
Core Extraction Flow in handleMetadataExtraction
When a worker picks up a metadata job, handleMetadataExtraction executes a twelve-step validation and persistence flow:
-
Configuration check – Verifies metadata extraction is enabled via
isMetadataExtracted. -
Asset loading – Fetches the asset via
assetJobRepository.getForMetadataExtraction. -
Tag reading – Reads EXIF from the original file, side-car XMP via
metadataRepository.readTags, and video streams viamediaRepository.probe. -
Tag merging – Side-car dates overwrite media dates; video duration is stripped for non-animated images to prevent data pollution.
-
Date normalization –
firstDateTimescans ordered EXIF date tags (DateTimeOriginal, CreateDate, ModifyDate) and returns the first validExifDateTime. ThegetDateshelper normalizes time-zones (fallback to UTC) and falls back to file-system timestamps when EXIF dates are missing. -
Dimension extraction –
getImageDimensionsprefersImageSizefor RAW formats, otherwise usesImageWidth/ImageHeighttags. -
Data validation – Helpers like
validate,validateRange,getLensModel, andgetBitsPerSampleensure numeric values fit PostgreSQL integer limits before populatingAssetExifTable. -
Database persistence – Upserts the EXIF record via
assetRepository.upsertExifand updates the asset record with duration, local time, and hidden dimensions. -
Side-car writeback – If tags changed,
metadataRepository.writeTagsupdates the XMP side-car file. -
Event emission – Fires
AssetMetadataExtractedand marks the extraction timestamp viaassetRepository.upsertJobStatus.
Motion Photos, Live Photos, and Face Tags
The service executes three critical side-effects in a Tasks batch after primary EXIF extraction:
-
Motion-photo extraction –
applyMotionPhotosextracts embedded video bytes from motion-photo containers, creates hidden video assets, links them viamotionPhotoVideoId, and queues encode jobs. -
Live-photo linking –
linkLivePhotospairs matching photo/video assets usingContentIdentifiertags to establishlivePhotoVideoIdrelationships. -
Face tag import – When
metadata.faceImport.enabledis true,hasTaggedFacesdiscoversRegionInfotags in XMP data,orientRegionInfoadjusts coordinates based on EXIF orientation, and creates/updatesPersonandAssetFacerows.
OCR Text Extraction Pipeline
The OCR pipeline mirrors the metadata architecture but targets searchable text extraction using machine learning models.
Queue Management and Job Flow
OcrService.handleQueueOcr streams assets with preview images via assetJobRepository.streamForOcrJob and queues JobName.Ocr jobs. When force is true, existing OCR data is purged first via ocrRepository.deleteAll.
// OcrService queues OCR jobs for assets with preview files
await jobRepository.queue({
name: JobName.Ocr,
data: { id: assetId },
});
OCR Processing and Search Tokenization
When executing handleOcr, the service performs:
-
Enablement check – Validates OCR via
isOcrEnabledin the machine-learning configuration. -
Asset validation – Loads the asset via
assetJobRepository.getForOcr, ensuringpreviewFileexists (the thumbnail used for OCR). -
ML inference – Calls
machineLearningRepository.ocrto send the preview image to the configured OCR provider (e.g., Tesseract). -
Result parsing –
parseOcrResultsextracts bounding box arrays (boxproperty) and confidence scores intoocrDataListentries. -
Search preparation –
tokenizeForSearch(fromserver/src/utils/database.ts) splits detected text on whitespace and punctuation, lower-casing tokens for PostgreSQL full-text search compatibility. -
Persistence –
ocrRepository.upsertstores per-box rows with bounding coordinates and the concatenatedsearchTexttoken string.
The searchText column enables full-text search via PostgreSQL tsvector indexing, allowing users to locate assets by text content captured in images.
Database Integration and Search Architecture
Both services write to distinct PostgreSQL tables optimized for query performance:
-
EXIF data persists to
asset_exifwith columns for GPS coordinates, camera make/model, lens information, and orientation flags. -
OCR data writes to the
ocrtable with geometric bounding boxes and the tokenizedsearchTextfield used bysearch.service.tsfortsvectorqueries.
The repositories abstract database access: metadataRepository wraps exiftool-vendored for EXIF I/O, while ocrRepository handles geometric data and search token persistence.
Practical Code Examples
Manually Triggering Metadata Extraction
import { JobName } from 'src/interfaces/job.interface';
// Queue a single asset for EXIF extraction
await metadataService.handleQueueMetadataExtraction();
// Or directly queue a job:
await jobRepository.queue({
name: JobName.AssetExtractMetadata,
data: { id: 'c1a2b3d4-...' },
});
Direct OCR Invocation
// Process OCR immediately for a specific asset
await ocrService.handleOcr({ id: 'c1a2b3d4-...' });
Querying Extracted Metadata
// Read EXIF data using Kysely query builder
const exif = await db
.selectFrom('asset_exif')
.select(['make', 'model', 'city', 'country', 'dateTimeOriginal'])
.where('assetId', '=', assetId)
.executeTakeFirst();
console.log(`Shot with ${exif?.make} ${exif?.model} in ${exif?.city}`);
Searching by OCR Content
// Search service queries the tsvector index on ocr.search_text
const results = await searchService.search({
query: 'invoice 2025',
ocr: true, // restricts to OCR text matches
});
Summary
-
MetadataService (
server/src/services/metadata.service.ts) handles EXIF extraction, side-car XMP management, motion-photo video extraction, live-photo linking, and face-tag import viahandleMetadataExtraction. -
OcrService (
server/src/services/ocr.service.ts) processes preview images throughhandleOcr, callingmachineLearningRepository.ocrand tokenizing results viatokenizeForSearchfor full-text search. -
Both services use NestJS job queues (
QueueName.MetadataExtraction,QueueName.Ocr) with pagination constants fromJOBS_ASSET_PAGINATION_SIZEto process assets in manageable batches. -
EXIF data validates numeric ranges before persistence to PostgreSQL, while OCR data includes geometric bounding boxes and search-token strings for
tsvectorindexing.
Frequently Asked Questions
How does Immich handle time-zones when EXIF data is missing GPS offset?
When GPS-based time-zone data is unavailable, MetadataService uses the getDates helper to normalize timestamps to UTC as a fallback. The system first attempts to extract OffsetTime, OffsetTimeOriginal, or OffsetTimeDigitized from EXIF; if these are missing, it falls back to the file-system modification time while preserving the original localDateTime in the asset record.
Can Immich extract text from scanned documents or screenshots?
Yes. The OcrService processes any asset with a generated previewFile, including scanned PDFs, screenshots, and photographs of documents. The machineLearningRepository.ocr method sends the preview image to the configured OCR model (Tesseract or external provider), and parseOcrResults extracts bounding boxes and confidence scores for searchable storage in the ocr table.
What happens to motion-photo video data during metadata extraction?
MetadataService.applyMotionPhotos extracts embedded video bytes from motion-photo formats (Samsung, Google Pixel), creates a hidden video asset linked via the motionPhotoVideoId foreign key, and queues an encode job for the extracted video. This occurs within the Tasks batch after primary EXIF extraction completes, ensuring the video is searchable and thumbnail-ready.
Does modifying EXIF tags in Immich update the side-car XMP files?
Yes. When MetadataService detects tag modifications during processing, it invokes metadataRepository.writeTags to persist changes back to side-car XMP files if they exist. This ensures that external tools reading the side-car files see the same metadata that Immich displays in its interface.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →