Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
67 changes: 67 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,73 @@ jobs:
- name: đź§Ş Run PHPUnit
run: vendor/bin/phpunit --colors=always

# Les règles de CONTRIBUTING.md ne valent que si une machine les vérifie.
conventions:
name: 📏 Conventions
runs-on: ubuntu-latest

steps:
- name: 📥 Checkout code
uses: actions/checkout@v5

- name: đźš§ No Symfony outside the bridge
run: |
# La librairie doit rester utilisable hors Symfony : c'est la raison
# d'ĂŞtre d'un type: library plutĂ´t qu'un symfony-bundle.
found=$(grep -rl 'Symfony\\' src/ --include='*.php' | grep -v '^src/Bridge/' || true)
if [ -n "$found" ]; then
echo "::error::Symfony importé hors de src/Bridge/ :"; echo "$found"; exit 1
fi
echo "✅ Symfony confiné à src/Bridge/"

- name: đźš§ No GD in src/
run: |
# GD sert Ă  fabriquer et valider dans les tests, jamais Ă  extraire :
# le package ne doit dépendre d'aucune extension.
found=$(grep -rlE 'imagecreate|imagejpeg|getimagesize|imagedestroy' src/ --include='*.php' || true)
if [ -n "$found" ]; then
echo "::error::GD utilisé dans src/ :"; echo "$found"; exit 1
fi
echo "âś… aucun appel GD dans src/"

- name: đźš§ declare(strict_types=1) everywhere
run: |
found=$(find src -name '*.php' ! -exec grep -lq 'declare(strict_types=1);' {} \; -print)
if [ -n "$found" ]; then
echo "::error::declare(strict_types=1) manquant :"; echo "$found"; exit 1
fi
echo "âś… strict_types dans tous les fichiers de src/"

- name: đźš§ No CPU-dependent unpack
run: |
# S et L suivent l'endianness de la machine : le parsing deviendrait
# dépendant du CPU. Seuls v/V (little) et n/N/J (big) sont admis.
found=$(grep -rnE "unpack\('[SL]" src/ --include='*.php' || true)
if [ -n "$found" ]; then
echo "::error::unpack() dépendant du CPU :"; echo "$found"; exit 1
fi
echo "âś… endianness explicite partout"

- name: đźš§ Public API documented in English
run: |
# Le package est public : son PHPDoc est sa documentation d'API.
found=$(grep -rlP '^\s*\*.*\b(le|la|les|une|des|du|qui|que|dans|pour|avec|sur|est|sont|cette|ce)\b' \
src/ --include='*.php' || true)
if [ -n "$found" ]; then
echo "::error::commentaires en français dans src/ :"; echo "$found"; exit 1
fi
echo "âś… commentaires en anglais"

- name: đźš§ No RAW committed
run: |
# Un RAW porte des EXIF (numéro de série, GPS) et resterait dans
# l'historique git Ă  vie.
found=$(git ls-files | grep -iE '\.(cr2|cr3|nef|arw|dng|raf|orf|rw2)$' || true)
if [ -n "$found" ]; then
echo "::error::fichier RAW commité :"; echo "$found"; exit 1
fi
echo "âś… aucun RAW dans le repo"

coverage:
name: 📊 Couverture (≥ 95 %)
runs-on: ubuntu-latest
Expand Down
35 changes: 25 additions & 10 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -271,13 +271,28 @@ iPhone 6s Plus iPhone 7 Plus iPhone SE iPhone XS iPhone 12 Pro

### Known to have no preview

These files contain **no JPEG at all** — verified byte by byte. `PreviewNotFoundException`
is the correct response, not a defect:

| Model | What the file actually holds |
|-------------------------------------------------|-------------------------------------------|
| Nikon D1H (2001) | uncompressed 160Ă—120 RGB thumbnail |
| Nikon COOLSCAN V ED | film scanner, no camera preview |
| Apple iPhone 8 | 64 sensor tiles, no JPEG |
| Canon PowerShot SX100 IS, SX510 HS, ELPH 130 IS | CHDK DNGs, uncompressed 128Ă—96 thumbnail |
| Canon PowerShot G7 X, IXY 220F | `.CRW` — pre-CR2 format, out of scope |
These models throw `PreviewNotFoundException` — **and that is the correct answer**. Their
files contain no JPEG whatsoever, which is verifiable in one command:

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

| Model | What the file actually holds |
|-------------------------------------------------|------------------------------------|
| Nikon D1H (2001) | uncompressed 160Ă—120 RGB thumbnail |
| Nikon COOLSCAN V ED | film scanner output |
| Apple iPhone 8 | 64 sensor tiles, no JPEG |
| Canon PowerShot SX100 IS, SX510 HS, ELPH 130 IS | uncompressed 128Ă—96 thumbnail |
| Canon PowerShot G7 X, IXY 220F | `.CRW` — pre-CR2, out of scope |

Two 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 %.**
14 changes: 7 additions & 7 deletions src/Bridge/Symfony/RawPreviewExtractorBundle.php
Original file line number Diff line number Diff line change
Expand Up @@ -11,16 +11,16 @@
use Symfony\Component\HttpKernel\Bundle\AbstractBundle;

/**
* Bundle optionnel : enregistre l'extracteur comme service auto-wirable.
* Optional bundle: registers the extractor as an autowirable service.
*
* Il se limite au **câblage** — aucune logique métier, aucun parsing. La
* librairie fonctionne Ă  l'identique sans lui, en Laravel comme en PHP nu :
* It limits itself to **wiring** — no business logic, no parsing. The library
* works identically without it, in Laravel as in plain PHP:
*
* ```php
* $extractor = RawPreviewExtractor::createDefault();
* ```
*
* Activation (Flex le fait automatiquement) :
* Activation (Flex does it automatically):
*
* ```php
* // config/bundles.php
Expand All @@ -29,7 +29,7 @@
* ];
* ```
*
* `RawPreviewExtractorInterface` devient alors injectable par autowiring.
* `RawPreviewExtractorInterface` then becomes injectable through autowiring.
*/
final class RawPreviewExtractorBundle extends AbstractBundle
{
Expand All @@ -41,8 +41,8 @@ public function loadExtension(
ContainerConfigurator $container,
ContainerBuilder $builder,
): void {
// Chemin physique : la notation @Bundle est dépréciée pour les bundles
// réutilisables.
// Physical path: the @Bundle notation is deprecated for reusable
// bundles.
$loader = new PhpFileLoader($builder, new FileLocator(__DIR__ . '/Resources/config'));
$loader->load('services.php');
}
Expand Down
20 changes: 10 additions & 10 deletions src/Bridge/Symfony/Resources/config/services.php
Original file line number Diff line number Diff line change
Expand Up @@ -13,11 +13,11 @@
use function Symfony\Component\DependencyInjection\Loader\Configurator\service;

/*
* Définitions explicites, sans autowiring ni autoconfiguration.
* Explicit definitions, without autowiring or autoconfiguration.
*
* C'est la règle bundle la plus souvent violée : un bundle réutilisable câble
* ses services Ă  la main. Activer l'autowiring ici polluerait la configuration
* de l'application consommatrice.
* This is the most often violated bundle rule: a reusable bundle wires its
* services by hand. Enabling autowiring here would pollute the configuration of
* the consuming application.
*/
return static function (ContainerConfigurator $container): void {
$services = $container->services();
Expand All @@ -26,10 +26,10 @@
$services->set('raw_preview_extractor.parser.tiff', TiffPreviewParser::class);
$services->set('raw_preview_extractor.parser.cr3', Cr3PreviewParser::class);

// La map qui porte l'OCP : ajouter RAF ou ORF en v2 se fera par une ligne
// ici, sans toucher à la façade. Les clés sont les valeurs de l'enum Format,
// construites explicitement — un tag auto-collecté exigerait de
// l'autoconfiguration, interdite dans un bundle.
// The map that carries the OCP: adding RAF or ORF in v2 will be done with a
// line here, without touching the facade. The keys are the values of the
// Format enum, built explicitly — an auto-collected tag would require
// autoconfiguration, which is forbidden in a bundle.
$services->set('raw_preview_extractor.extractor', RawPreviewExtractor::class)
->args([
service('raw_preview_extractor.format_detector'),
Expand All @@ -42,8 +42,8 @@
],
]);

// Seule l'interface est exposée : les parseurs et le détecteur restent
// privés, ce sont des détails d'implémentation.
// Only the interface is exposed: the parsers and the detector stay private,
// they are implementation details.
$services->alias(RawPreviewExtractorInterface::class, 'raw_preview_extractor.extractor')
->public();
};
10 changes: 5 additions & 5 deletions src/Exception/CorruptedFileException.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,13 @@
namespace RonanLenouvel\RawPreviewExtractor\Exception;

/**
* Le fichier est illisible ou structurellement invalide.
* The file is unreadable or structurally invalid.
*
* Couvre : fichier absent ou non lisible, en-tête absent ou tronqué, offset
* pointant hors du fichier, taille absurde, structure incohérente.
* Covers: missing or unreadable file, missing or truncated header, offset
* pointing outside the file, absurd size, inconsistent structure.
*
* À distinguer de {@see PreviewNotFoundException} : un fichier **tronqué** est
* corrompu, pas dépourvu de preview. La distinction est testée — ne la brouille pas.
* To be distinguished from {@see PreviewNotFoundException}: a **truncated** file
* is corrupted, not devoid of a preview. The distinction is tested — do not blur it.
*/
final class CorruptedFileException extends \RuntimeException implements RawPreviewExtractorException
{
Expand Down
10 changes: 5 additions & 5 deletions src/Exception/PreviewNotFoundException.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,13 @@
namespace RonanLenouvel\RawPreviewExtractor\Exception;

/**
* Le fichier est valide, mais ne contient aucune preview JPEG exploitable.
* The file is valid, but contains no usable JPEG preview.
*
* Cas typiques : aucun tag ne désigne de JPEG, ou la taille annoncée est nulle.
* Typical cases: no tag points to a JPEG, or the announced size is zero.
*
* À distinguer de {@see CorruptedFileException} : un fichier **tronqué** ou dont
* un offset ment est corrompu, pas dépourvu de preview. Cette frontière est
* testée — ne la brouille pas.
* To be distinguished from {@see CorruptedFileException}: a **truncated** file, or
* one whose offset lies, is corrupted, not devoid of a preview. This boundary is
* tested — do not blur it.
*/
final class PreviewNotFoundException extends \RuntimeException implements RawPreviewExtractorException
{
Expand Down
10 changes: 5 additions & 5 deletions src/Exception/RawPreviewExtractorException.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,20 +5,20 @@
namespace RonanLenouvel\RawPreviewExtractor\Exception;

/**
* Interface marqueur commune Ă  toutes les exceptions du package.
* Marker interface common to every exception of the package.
*
* Elle permet à l'appelant de dégrader proprement avec un seul `catch`, sans
* connaître le détail des cas d'échec :
* It lets the caller degrade gracefully with a single `catch`, without knowing
* the detail of the failure cases:
*
* ```php
* try {
* $preview = $extractor->extract($path);
* } catch (RawPreviewExtractorException) {
* // pas de vignette : on continue sans
* // no thumbnail: carry on without one
* }
* ```
*
* Toute exception levée par ce package l'implémente — c'est un invariant testé.
* Every exception thrown by this package implements it — a tested invariant.
*/
interface RawPreviewExtractorException extends \Throwable
{
Expand Down
14 changes: 7 additions & 7 deletions src/Exception/UnsupportedFormatException.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,15 +5,15 @@
namespace RonanLenouvel\RawPreviewExtractor\Exception;

/**
* Le fichier n'est pas un RAW pris en charge par ce package.
* The file is not a RAW supported by this package.
*
* Deux cas mènent ici : la signature n'est reconnue comme aucun format connu,
* ou le format est reconnu mais aucun parseur ne lui est associé. Du point de
* vue de l'appelant, les deux reviennent au même — le fichier ne peut pas être
* traité.
* Two cases lead here: the signature is not recognised as any known format, or
* the format is recognised but no parser is associated with it. From the
* caller's point of view, both amount to the same thing — the file cannot be
* processed.
*
* Utilisez {@see \RonanLenouvel\RawPreviewExtractor\RawPreviewExtractorInterface::supports()}
* pour l'éviter sans passer par un `try`/`catch`.
* Use {@see \RonanLenouvel\RawPreviewExtractor\RawPreviewExtractorInterface::supports()}
* to avoid it without going through a `try`/`catch`.
*/
final class UnsupportedFormatException extends \RuntimeException implements RawPreviewExtractorException
{
Expand Down
16 changes: 8 additions & 8 deletions src/ExtractedPreview.php
Original file line number Diff line number Diff line change
Expand Up @@ -7,24 +7,24 @@
use RonanLenouvel\RawPreviewExtractor\Format\Format;

/**
* Une preview JPEG extraite d'un fichier RAW.
* A JPEG preview extracted from a RAW file.
*
* ```php
* $preview = $extractor->extract('/photos/IMG_0042.CR2');
* file_put_contents('/cache/thumb.jpg', $preview->jpegData);
* ```
*
* Les dimensions proviennent du **JPEG lui-mĂŞme**, pas des tags du conteneur :
* ceux-ci décrivent parfois l'image RAW pleine résolution et non la preview.
* Dimensions come from the **JPEG itself**, not from container tags: those often
* describe the full-resolution RAW image rather than the preview.
*/
final readonly class ExtractedPreview
{
/**
* @param string $jpegData JPEG binaire brut, prêt à écrire sur disque ;
* son magic `FFD8` est validé à l'extraction
* @param int $width largeur de la preview, en pixels
* @param int $height hauteur de la preview, en pixels
* @param Format $sourceFormat format du RAW dont elle provient
* @param string $jpegData raw JPEG bytes, ready to write to disk; the
* `FFD8` magic is validated during extraction
* @param int $width preview width in pixels
* @param int $height preview height in pixels
* @param Format $sourceFormat format of the RAW file it came from
*/
public function __construct(
public string $jpegData,
Expand Down
20 changes: 10 additions & 10 deletions src/Format/Format.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,27 +5,27 @@
namespace RonanLenouvel\RawPreviewExtractor\Format;

/**
* Formats RAW supportés par le package.
* RAW formats supported by the package.
*
* La valeur de chaque cas est l'extension usuelle du format, en minuscules.
* Elle sert d'identifiant stable — notamment comme clé de la map
* Format → PreviewParserInterface — et non de critère de détection : celle-ci
* repose exclusivement sur la signature binaire du fichier.
* The value of each case is the format's usual extension, in lowercase. It acts
* as a stable identifier — notably as the key of the
* Format → PreviewParserInterface map — and not as a detection criterion:
* detection relies exclusively on the file's binary signature.
*/
enum Format: string
{
/** Canon RAW v2 — conteneur TIFF. */
/** Canon RAW v2 — TIFF container. */
case CR2 = 'cr2';

/** Canon RAW v3 — conteneur ISO-BMFF. */
/** Canon RAW v3 — ISO-BMFF container. */
case CR3 = 'cr3';

/** Nikon Electronic Format — conteneur TIFF. */
/** Nikon Electronic Format — TIFF container. */
case NEF = 'nef';

/** Sony Alpha RAW — conteneur TIFF. */
/** Sony Alpha RAW — TIFF container. */
case ARW = 'arw';

/** Adobe Digital Negative — conteneur TIFF. */
/** Adobe Digital Negative — TIFF container. */
case DNG = 'dng';
}
Loading