PHPackages                             ronanlenouvel/raw-preview-extractor - PHPackages - PHPackages  [Skip to content](#main-content)[PHPackages](/)[Directory](/)[Categories](/categories)[Trending](/trending)[Leaderboard](/leaderboard)[Changelog](/changelog)[Analyze](/analyze)[Collections](/collections)[Log in](/login)[Sign up](/register)

1. [Directory](/)
2. /
3. [Image &amp; Media](/categories/media)
4. /
5. ronanlenouvel/raw-preview-extractor

ActiveLibrary[Image &amp; Media](/categories/media)

ronanlenouvel/raw-preview-extractor
===================================

Extract embedded JPEG previews from camera RAW files (CR2, CR3, NEF, ARW, DNG) in pure PHP, no external binaries.

1.2.0(1mo ago)0191↓75%[2 issues](https://github.com/ronan-develop/raw-preview-extractor/issues)[1 PRs](https://github.com/ronan-develop/raw-preview-extractor/pulls)MITPHPPHP &gt;=8.2CI passing

Since Jul 16Pushed 1mo agoCompare

[ Source](https://github.com/ronan-develop/raw-preview-extractor)[ Packagist](https://packagist.org/packages/ronanlenouvel/raw-preview-extractor)[ RSS](/packages/ronanlenouvel-raw-preview-extractor/feed)WikiDiscussions main Synced 1w ago

READMEChangelog (2)Dependencies (6)Versions (6)Used By (0)

Raw Preview Extractor
=====================

[](#raw-preview-extractor)

Extract the JPEG preview embedded in camera RAW files — **pure PHP, no external binaries**.

[![CI](https://github.com/ronan-develop/raw-preview-extractor/actions/workflows/ci.yml/badge.svg)](https://github.com/ronan-develop/raw-preview-extractor/actions/workflows/ci.yml)[![License: MIT](https://camo.githubusercontent.com/784362b26e4b3546254f1893e778ba64616e362bd6ac791991d2c9e880a3a64e/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f4c6963656e73652d4d49542d677265656e2e737667)](LICENSE)

> **Status: feature-complete and validated against real camera files** (see [Tested cameras](#tested-cameras)). The public API is settled, but a released version is a semver commitment, and more cameras are worth testing first.

Why
---

[](#why)

Displaying a thumbnail for a RAW file normally means decoding it, which requires `libraw`or Imagick — unavailable on shared hosting without root access.

Almost every modern camera already embeds a full JPEG preview inside its RAW files. This library **locates and extracts that JPEG** by reading the container structure. No demosaicing, no sensor data, no dependencies: just byte reading.

Supported formats
-----------------

[](#supported-formats)

FormatContainerDetected byCR2 (Canon)TIFF 6.0`CR` signatureCR3 (Canon)ISO-BMFF`ftyp` + `crx` brandNEF (Nikon)TIFF 6.0`Make` tagARW (Sony)TIFF 6.0`Make` tagDNG (Adobe)TIFF 6.0`DNGVersion` tagDetection reads the file's **binary signature**, never its extension: a `.cr2` renamed to `.jpg` is still detected as a CR2, and a `.cr2` that isn't one is rejected.

RAF (Fuji) and ORF (Olympus) are planned for v2.

Requirements
------------

[](#requirements)

PHP &gt;= 8.2. That's it — the `require` section depends on nothing else.

The optional Symfony bundle supports **6.4 (LTS), 7.x and 8.x**. Composer picks whichever matches your application; the library itself never depends on Symfony.

Installation
------------

[](#installation)

```
composer require ronanlenouvel/raw-preview-extractor
```

Usage
-----

[](#usage)

```
use RonanLenouvel\RawPreviewExtractor\Exception\RawPreviewExtractorException;
use RonanLenouvel\RawPreviewExtractor\RawPreviewExtractor;

$extractor = RawPreviewExtractor::createDefault();

try {
    $preview = $extractor->extract('/path/to/photo.cr2');

    file_put_contents('/path/to/thumbnail.jpg', $preview->jpegData);

    echo $preview->width, 'x', $preview->height;
    echo $preview->sourceFormat->name;
} catch (RawPreviewExtractorException) {
    // Every exception thrown by this package implements this interface,
    // so a single catch is enough to degrade gracefully.
}
```

### Orientation

[](#orientation)

A preview is stored exactly as the sensor captured it: a shot taken in portrait comes out **lying on its side**. The camera records the rotation rather than applying it.

This library does not rotate the image — that would need GD, the very dependency it exists to avoid. It tells you what the file says:

```
$preview->orientation;                  // Orientation::Rotate90
$preview->orientation->degrees();       // 90
$preview->orientation->isUpright();     // false
$preview->orientation->isMirrored();    // false — 4 of the 8 EXIF values are mirrors
$preview->orientation->swapsDimensions();  // true — width and height swap on a quarter turn
```

In a browser, fixing it costs nothing:

```
printf('',
    $preview->orientation->degrees());
```

Server-side, if you do have GD — note it rotates counter-clockwise:

```
$image = imagecreatefromstring($preview->jpegData);

if (!$preview->orientation->isUpright()) {
    $image = imagerotate($image, -$preview->orientation->degrees(), 0);
}
```

Check support before extracting:

```
if ($extractor->supports($path)) {
    // …
}
```

Processing a directory — the whole library in one loop:

```
$extractor = RawPreviewExtractor::createDefault();

foreach (glob('/photos/*') as $path) {
    try {
        $preview = $extractor->extract($path);
        file_put_contents("/thumbs/" . basename($path) . '.jpg', $preview->jpegData);
    } catch (RawPreviewExtractorException) {
        continue;  // not a RAW, no preview, or corrupt — skip it
    }
}
```

Extraction is O(1) in file size: a 30 MB RAW takes the same ~2 ms as a 7 MB one, because the file is never loaded — only its container structure is read, then the JPEG block is seeked to directly.

### Metadata

[](#metadata)

Alongside the preview, the extractor reads the **shooting settings** from the RAW's EXIF — what a photographer wants next to the image: when it was taken, and how.

```
$preview = $extractor->extract('/path/to/photo.cr2');

$meta = $preview->metadata;              // RawMetadata|null

$meta?->dateTimeOriginal;                // "2024:06:15 12:30:45" (raw EXIF form)
$meta?->fNumber;                         // 2.8   (aperture, float)
$meta?->exposureTime;                    // "1/250" (shutter, fraction kept intact)
$meta?->iso;                             // 400
$meta?->focalLength;                     // 50.0  (millimetres)
$meta?->lensModel;                       // "RF50mm F1.8 STM"
$meta?->cameraMake;                      // "Canon"
$meta?->cameraModel;                     // "Canon EOS R5"
```

Every field is nullable, and so is `$preview->metadata` itself: a tag may be absent, an old body says less than a recent one, and a partial read is never a failure. `RawMetadata::isEmpty()`tells a caller nothing usable was found, in one call:

```
if ($meta !== null && !$meta->isEmpty()) {
    // display the shooting settings
}
```

Values are exposed exactly as the file encodes them — no rounding, no locale: the shutter speed keeps its fraction (`"1/250"` reads better than `0.004`), the aperture stays a plain number. Metadata is read from the same container walk as the preview, so it costs nothing extra.

> Coverage follows the TIFF-based formats (CR2, NEF, ARW, DNG), which carry EXIF in a standard IFD. For CR3 (ISO-BMFF) the preview is returned but `metadata` is currently `null`.

### Exceptions

[](#exceptions)

All of them implement `RawPreviewExtractorException`:

ExceptionMeaning`UnsupportedFormatException`not a supported RAW file`PreviewNotFoundException`valid file, but no embedded JPEG preview`CorruptedFileException`unreadable or structurally invalid fileSymfony integration (optional)
------------------------------

[](#symfony-integration-optional)

The package ships a thin, optional bundle. The core library itself knows nothing about Symfony and works in any PHP project.

```
// config/bundles.php
return [
    // …
    RonanLenouvel\RawPreviewExtractor\Bridge\Symfony\RawPreviewExtractorBundle::class => ['all' => true],
];
```

`RawPreviewExtractorInterface` then becomes autowirable.

Tested cameras
--------------

[](#tested-cameras)

Verified against real files from [raw.pixls.us](https://raw.pixls.us/) (CC0):

All five formats are verified against real camera files:

CameraYearFormatFilePreview extractedCanon EOS 5D2005CR213 MB2496×1664 — 1656 KBCanon EOS 5D Mark II2008CR227 MB5616×3744 — 1980 KBCanon 5D Mark II sRAW12008CR215 MB5616×3744 — 1973 KBCanon EOS 5D Mark IV2016CR262 MB6720×4480 — 2047 KBCanon EOS R2018CR330 MB1620×1080 — 228 KBCanon EOS RP2019CR37 MB1620×1080 — 328 KBNikon D7502014NEF25 MB6016×4016 — 952 KBSony α7 (ILCE-7)2013ARW24 MB1616×1080 — 460 KBApple iPhone 12 Pro2020DNG29 MB4032×3024 — 5239 KBFour generations of the same Canon 5D line — 2005 to 2016, 12 to 30 megapixels — are covered deliberately: RAW layout drifts between generations, and that drift is where format parsers break.

### Wider audit

[](#wider-audit)

Beyond the table above, the library is audited against the [raw.pixls.us](https://raw.pixls.us/) catalogue — 400+ camera models, CC0 files:

```
php bin/audit-cameras.php Canon 25    # 25 Canon models, spread across the range
php bin/audit-cameras.php all 500     # everything
```

Each file is downloaded, extracted, verified with `imagecreatefromstring()`, then deleted. Latest run — **72 models across the four brands in scope**:

BrandPassedNotesSony23/23100 % — A290 (2010) through FX30Nikon20/2290.9 %Apple5/683.3 %Canon16/2176.2 % — every EOS body passed**Every failure was verified by hand, and every one is correct**: those files contain no JPEG preview at all (zero `FFD8` bytes in the entire file). A Nikon D1H from 2001 carries an uncompressed 160×120 RGB thumbnail; CHDK-generated compact DNGs do the same; legacy `.CRW`files predate CR2 and are out of scope. `PreviewNotFoundException` is the right answer, so the real success rate on files that *have* a preview is 100 %.

The full list is at the [bottom of this page](#appendix-models-verified-against-real-files).

Every preview above is checked with `imagecreatefromstring()` — not just decoded headers, but actually opened. Extraction takes **1–9 ms** on files up to 62 MB: the file is never loaded into memory, only the container structure is read before seeking to the JPEG block.

RAW layout varies between camera generations, and CR3 has no public specification. If your camera is not listed, the library may well work — it just has not been verified. You can check in one command: see [Trying it on your own RAW files](#trying-it-on-your-own-raw-files).

Contributing
------------

[](#contributing)

See [CONTRIBUTING.md](CONTRIBUTING.md) for conventions, the TDD workflow, and the binary parsing rules. In short:

```
composer install
vendor/bin/phpunit                            # full suite
vendor/bin/phpunit --testsuite unit           # fast: forged bytes, no files
vendor/bin/phpunit --testsuite integration    # public API end to end
```

The suite runs on a clean machine with **no RAW file whatsoever**: tests build byte sequences in memory. No camera files are committed to this repository — a RAW carries EXIF data (serial number, sometimes GPS), and a committed binary stays in git history forever.

### Trying it on your own RAW files

[](#trying-it-on-your-own-raw-files)

Synthetic tests prove the parsing is correct; they cannot prove your camera lays out its IFDs the way this library expects. To check for yourself:

```
mkdir -p tests/Fixtures/local        # git-ignored
cp ~/photos/IMG_0042.CR2 tests/Fixtures/local/

php bin/extract-local.php
```

```
FICHIER                      FORMAT       DIMENSIONS     TAILLE   DURÉE
──────────────────────────────────────────────────────────────────────────
✅ nikon-d750.nef            NEF           6016x4016     952 Ko     2 ms
✅ canon-eos-r.cr3           CR3           1620x1080     228 Ko     1 ms

```

Extracted previews are written to `tests/Fixtures/local/output/` — **open them**. A JPEG that decodes is not necessarily the right JPEG: this library once returned a valid 160×120 thumbnail where a 1620×1080 preview was expected, and every test was green.

Neither the RAW files nor the extracted previews are committed. The script itself lives in `bin/`, excluded from the published archive by `.gitattributes`.

Free CC0 sample files for most cameras: [raw.pixls.us](https://raw.pixls.us/).

License
-------

[](#license)

MIT — see [LICENSE](LICENSE).

`exiftool` and `libraw` were used as behavioural references only. No code was copied from either project.

---

Appendix: models verified against real files
--------------------------------------------

[](#appendix-models-verified-against-real-files)

Every model below had a real file downloaded, its preview extracted, and the result opened with `imagecreatefromstring()`. Sources: [raw.pixls.us](https://raw.pixls.us/) (CC0).

**Canon** — CR2 &amp; CR3

```
EOS 5D            EOS 5D Mark II    EOS 5D Mark II sRAW1   EOS 5D Mark IV
EOS-1D X Mark II  EOS 30D           EOS 100D               EOS 500D
EOS 1000D         EOS Rebel T5      EOS M6                 EOS Kiss M
EOS R             EOS RP            EOS R5                 EOS R100
PowerShot G12     PowerShot G1 X Mark II                   PowerShot SX1 IS
PowerShot V1

```

**Nikon** — NEF

```
D60      D90      D300     D610     D800     D850     D2Xs     D3300
D4       D5100    D5600    D6       D7500    E8400    Z 7      Z 30
1 AW1    1 J3     1 S2     Coolpix A

```

**Sony** — ARW

```
DSLR-A290    DSLR-A380    DSLR-A550    DSLR-A850    SLT-A35      SLT-A58
NEX-5N       NEX-7        ILCE-5000    ILCE-6100    ILCE-6600    ILCE-7S
ILCE-7M4     ILCE-7RM3A   ILCE-7CM2    ILCA-99M2    ILME-FX30    UMC-R10C
DSC-RX0      DSC-RX100M4  DSC-RX100M7  DSC-RX10M4   DSC-RX1RM2

```

**Apple** — DNG (including ProRAW)

```
iPhone 6s Plus   iPhone 7 Plus   iPhone SE   iPhone XS   iPhone 12 Pro

```

### Known to have no preview

[](#known-to-have-no-preview)

These models throw `PreviewNotFoundException` — **and that is the correct answer**. Their files contain no JPEG whatsoever, which is verifiable in one command:

```
grep -c $'\xff\xd8\xff' some-file.dng    # 0 — not a single JPEG marker in the file
```

ModelWhat the file actually holdsNikon D1H (2001)uncompressed 160×120 RGB thumbnailNikon COOLSCAN V EDfilm scanner outputApple iPhone 864 sensor tiles, no JPEGCanon PowerShot SX100 IS, SX510 HS, ELPH 130 ISuncompressed 128×96 thumbnailCanon PowerShot G7 X, IXY 220F`.CRW` — pre-CR2, out of scopeTwo reasons, neither of them a parser bug. **The oldest bodies predate the convention**: a D1H from 2001 stores a raw RGB thumbnail because embedding a JPEG preview was not yet standard practice. **The compacts never had a RAW mode**: their DNGs come from [CHDK](https://chdk.fandom.com/), an alternative firmware that writes sensor data with a minimal thumbnail.

There is nothing to extract in those files, so throwing is right — returning something would mean fabricating it.

**Success rate on files that actually contain a preview: 100 %.**

###  Health Score

43

—

FairBetter than 89% of packages

Maintenance92

Actively maintained with recent releases

Popularity15

Limited adoption so far

Community6

Small or concentrated contributor base

Maturity50

Maturing project, gaining track record

 Bus Factor1

Top contributor holds 100% of commits — single point of failure

How is this calculated?**Maintenance (25%)** — Last commit recency, latest release date, and issue-to-star ratio. Uses a 2-year decay window.

**Popularity (30%)** — Total and monthly downloads, GitHub stars, and forks. Logarithmic scaling prevents top-heavy scores.

**Community (15%)** — Contributors, dependents, forks, watchers, and maintainers. Measures real ecosystem engagement.

**Maturity (30%)** — Project age, version count, PHP version support, and release stability.

###  Release Activity

Cadence

Every ~1 days

Total

3

Last Release

42d ago

### Community

Maintainers

![](https://www.gravatar.com/avatar/797b728411179131d9183c38e79e06c150052d8286b2b335840031b350f4fea6?d=identicon)[ron2cuba](/maintainers/ron2cuba)

---

Top Contributors

[![ronan-develop](https://avatars.githubusercontent.com/u/118799163?v=4)](https://github.com/ronan-develop "ronan-develop (34 commits)")

---

Tags

arwcr2cr3dngexifnefphotographyphppreviewprorawrawsymfonysymfony-bundlethumbnailtiffthumbnailjpegpreviewexiftiffrawdngcr2cr3nefarw

###  Code Quality

TestsPHPUnit

### Embed Badge

![Health badge](/badges/ronanlenouvel-raw-preview-extractor/health.svg)

```
[![Health](https://phpackages.com/badges/ronanlenouvel-raw-preview-extractor/health.svg)](https://phpackages.com/packages/ronanlenouvel-raw-preview-extractor)
```

###  Alternatives

[intervention/image

PHP Image Processing

14.4k220.0M2.8k](/packages/intervention-image)[picqer/php-barcode-generator

An easy to use, non-bloated, barcode generator in PHP. Creates SVG, PNG, JPG and HTML images from the most used 1D barcode standards.

1.8k30.6M140](/packages/picqer-php-barcode-generator)[intervention/image-laravel

Laravel Integration of Intervention Image

16210.8M228](/packages/intervention-image-laravel)[miljar/php-exif

Object-Oriented EXIF parsing

1481.5M18](/packages/miljar-php-exif)[lychee-org/php-exif

Object-Oriented EXIF parsing

1287.5k3](/packages/lychee-org-php-exif)[fileeye/pel

PHP Exif Library. A library for reading and writing Exif headers in JPEG and TIFF images using PHP.

218.6M3](/packages/fileeye-pel)

PHPackages © 2026

[Directory](/)[Categories](/categories)[Trending](/trending)[Changelog](/changelog)[Analyze](/analyze)
