diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 232e068..b422b43 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -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 diff --git a/README.md b/README.md index 09795d1..d50abac 100644 --- a/README.md +++ b/README.md @@ -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 %.** diff --git a/src/Bridge/Symfony/RawPreviewExtractorBundle.php b/src/Bridge/Symfony/RawPreviewExtractorBundle.php index 65627b8..c9dea9d 100644 --- a/src/Bridge/Symfony/RawPreviewExtractorBundle.php +++ b/src/Bridge/Symfony/RawPreviewExtractorBundle.php @@ -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 @@ -29,7 +29,7 @@ * ]; * ``` * - * `RawPreviewExtractorInterface` devient alors injectable par autowiring. + * `RawPreviewExtractorInterface` then becomes injectable through autowiring. */ final class RawPreviewExtractorBundle extends AbstractBundle { @@ -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'); } diff --git a/src/Bridge/Symfony/Resources/config/services.php b/src/Bridge/Symfony/Resources/config/services.php index a14d442..22a8a9b 100644 --- a/src/Bridge/Symfony/Resources/config/services.php +++ b/src/Bridge/Symfony/Resources/config/services.php @@ -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(); @@ -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'), @@ -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(); }; diff --git a/src/Exception/CorruptedFileException.php b/src/Exception/CorruptedFileException.php index 5c5ba30..10a04c6 100644 --- a/src/Exception/CorruptedFileException.php +++ b/src/Exception/CorruptedFileException.php @@ -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 { diff --git a/src/Exception/PreviewNotFoundException.php b/src/Exception/PreviewNotFoundException.php index eff9bcb..4c3831d 100644 --- a/src/Exception/PreviewNotFoundException.php +++ b/src/Exception/PreviewNotFoundException.php @@ -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 { diff --git a/src/Exception/RawPreviewExtractorException.php b/src/Exception/RawPreviewExtractorException.php index 49783e7..bccb2fb 100644 --- a/src/Exception/RawPreviewExtractorException.php +++ b/src/Exception/RawPreviewExtractorException.php @@ -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 { diff --git a/src/Exception/UnsupportedFormatException.php b/src/Exception/UnsupportedFormatException.php index baeffc9..6d47f5f 100644 --- a/src/Exception/UnsupportedFormatException.php +++ b/src/Exception/UnsupportedFormatException.php @@ -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 { diff --git a/src/ExtractedPreview.php b/src/ExtractedPreview.php index 9df9a18..9d3ab6b 100644 --- a/src/ExtractedPreview.php +++ b/src/ExtractedPreview.php @@ -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, diff --git a/src/Format/Format.php b/src/Format/Format.php index 6416620..f1e66c2 100644 --- a/src/Format/Format.php +++ b/src/Format/Format.php @@ -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'; } diff --git a/src/Format/FormatDetector.php b/src/Format/FormatDetector.php index 14d541b..05c3904 100644 --- a/src/Format/FormatDetector.php +++ b/src/Format/FormatDetector.php @@ -5,25 +5,25 @@ namespace RonanLenouvel\RawPreviewExtractor\Format; /** - * Détecte le format RAW par lecture de la signature binaire du fichier. + * Detects the RAW format by reading the file's binary signature. * - * L'extension du fichier n'est jamais consultée : un CR2 renommé en `.jpg` - * reste détecté comme un CR2, et un `.cr2` qui n'en est pas un est rejeté. + * The file extension is never consulted: a CR2 renamed to `.jpg` is still + * detected as a CR2, and a `.cr2` that is not one is rejected. * - * Deux familles de conteneurs sont reconnues : - * - TIFF 6.0 (CR2, NEF, ARW, DNG), discriminé par la signature Canon `CR` - * ou par les tags DNGVersion / Make de l'IFD0 ; - * - ISO-BMFF (CR3), discriminé par la boîte `ftyp` et le brand `crx `. + * Two container families are recognised: + * - TIFF 6.0 (CR2, NEF, ARW, DNG), discriminated by the Canon `CR` signature + * or by the DNGVersion / Make tags of the IFD0; + * - ISO-BMFF (CR3), discriminated by the `ftyp` box and the `crx ` brand. */ final class FormatDetector implements FormatDetectorInterface { - /** Octets suffisants pour couvrir l'en-tête TIFF, la signature CR2 et le ftyp d'un CR3. */ + /** Bytes enough to cover the TIFF header, the CR2 signature and a CR3's ftyp. */ private const HEADER_BYTES = 16; - /** Nombre magique du format TIFF, lu selon l'endianness du fichier. */ + /** Magic number of the TIFF format, read according to the file's endianness. */ private const TIFF_MAGIC = 42; - /** Au-delà, un IFD0 aussi long trahit un fichier corrompu ou hostile. */ + /** Beyond this, an IFD0 that long betrays a corrupted or hostile file. */ private const MAX_IFD0_ENTRIES = 512; private const TAG_MAKE = 0x010F; @@ -31,20 +31,20 @@ final class FormatDetector implements FormatDetectorInterface public function detect(string $path): ?Format { - // fopen() réussit sur un répertoire : c'est fread() qui échouerait ensuite, - // en émettant un notice. On écarte le cas ici plutôt que de le museler. + // fopen() succeeds on a directory: it is fread() that would fail next, + // emitting a notice. We rule the case out here rather than muzzling it. if (!is_file($path)) { return null; } - // is_file() a déjà écarté l'absent et le répertoire. + // is_file() has already ruled out the missing file and the directory. $handle = fopen($path, 'rb'); try { $header = fread($handle, self::HEADER_BYTES); - // fread renvoie moins que demandé en fin de fichier : sans ce contrôle, - // unpack() lirait des octets qui n'existent pas. + // fread returns less than requested at end of file: without this check, + // unpack() would read bytes that do not exist. if (!is_string($header) || strlen($header) < 8) { return null; } @@ -57,7 +57,7 @@ public function detect(string $path): ?Format } /** - * CR3 : boîte `ftyp` en tête, brand majeur `crx ` aux octets 8-11. + * CR3: `ftyp` box up front, major brand `crx ` at bytes 8-11. */ private function detectIsoBmff(string $header): ?Format { @@ -85,7 +85,7 @@ private function detectTiff($handle, string $header): ?Format return null; } - // CR2 : signature « CR » suivie de la version, juste après l'en-tête TIFF. + // CR2: "CR" signature followed by the version, right after the TIFF header. if (strlen($header) >= 10 && 'CR' === substr($header, 8, 2)) { return Format::CR2; } @@ -100,26 +100,26 @@ private function detectTiff($handle, string $header): ?Format } /** - * @return array{string, string}|null couple de formats unpack() {court, long} + * @return array{string, string}|null pair of unpack() formats {short, long} */ private function readEndianness(string $header): ?array { return match (substr($header, 0, 2)) { 'II' => ['v', 'V'], // little-endian 'MM' => ['n', 'N'], // big-endian - default => null, // ni l'un ni l'autre : pas un TIFF + default => null, // neither one: not a TIFF }; } /** - * Parcourt les entrées de l'IFD0 à la recherche d'un tag discriminant. + * Walks the IFD0 entries looking for a discriminating tag. * * @param resource $handle */ private function detectFromIfd0($handle, string $shortFormat, string $longFormat, int $ifdOffset): ?Format { - // fseek au-delà de la fin réussit sur un fichier : c'est la lecture - // suivante qui rend une chaîne vide, ce qui est traité juste après. + // fseek past the end succeeds on a file: it is the next read that + // returns an empty string, which is handled right after. fseek($handle, $ifdOffset); $countBytes = fread($handle, 2); @@ -145,7 +145,7 @@ private function detectFromIfd0($handle, string $shortFormat, string $longFormat $tag = $this->unpackInt($shortFormat, substr($entry, 0, 2)); - // DNGVersion suffit : le format est normalisé par Adobe. + // DNGVersion is enough: the format is standardised by Adobe. if (self::TAG_DNG_VERSION === $tag) { return Format::DNG; } @@ -159,8 +159,8 @@ private function detectFromIfd0($handle, string $shortFormat, string $longFormat } /** - * Lit la valeur du tag Make, stockée hors de l'entrée dès qu'elle dépasse - * 4 octets — ce qui est toujours le cas d'un nom de fabricant. + * Reads the value of the Make tag, stored outside the entry as soon as it + * exceeds 4 bytes — which is always the case for a manufacturer name. * * @param resource $handle */ @@ -173,17 +173,17 @@ private function readMake($handle, string $longFormat, string $entry): ?string return null; } - // La position courante doit être restaurée : la boucle appelante - // continue de lire les entrées séquentiellement. Sur un flux fichier - // local, ftell/fseek n'échouent pas — inutile de s'en garder. + // The current position must be restored: the calling loop keeps reading + // the entries sequentially. On a local file stream, ftell/fseek do not + // fail — no need to guard against it. $position = (int) ftell($handle); fseek($handle, $offset); $value = (string) fread($handle, $count); fseek($handle, $position); - // Un offset hors bornes ne fait pas échouer fseek : c'est la lecture - // qui rend une chaîne vide. + // An out-of-bounds offset does not make fseek fail: it is the read that + // returns an empty string. return '' === $value ? null : rtrim($value, "\x00"); } @@ -204,15 +204,14 @@ private function formatFromMake(?string $make): ?Format } /** - * Vérifie la longueur avant unpack() : un fread en fin de fichier rend moins - * d'octets que demandé, silencieusement. Une fois la longueur garantie, - * unpack() ne peut plus échouer. + * Checks the length before unpack(): a fread at end of file returns fewer + * bytes than requested, silently. Once the length is guaranteed, unpack() + * can no longer fail. */ private function unpackInt(string $format, string $bytes): ?int { - // Les octets viennent soit de l'en-tête (8 octets garantis), soit d'un - // fread dont la longueur est vérifiée par l'appelant : unpack ne peut - // pas travailler à court. + // The bytes come either from the header (8 bytes guaranteed), or from a + // fread whose length is checked by the caller: unpack cannot work short. return unpack($format, $bytes)[1]; } } diff --git a/src/Format/FormatDetectorInterface.php b/src/Format/FormatDetectorInterface.php index 81bcffe..718f968 100644 --- a/src/Format/FormatDetectorInterface.php +++ b/src/Format/FormatDetectorInterface.php @@ -5,19 +5,19 @@ namespace RonanLenouvel\RawPreviewExtractor\Format; /** - * Identifie le format RAW d'un fichier à partir de sa signature binaire. + * Identifies the RAW format of a file from its binary signature. * - * Un détecteur détecte : il ne juge pas. Un fichier illisible, absent ou - * non-RAW donne `null`, jamais une exception — c'est à l'appelant de décider - * si l'absence de format est une erreur. + * A detector detects: it does not judge. An unreadable, missing or non-RAW file + * yields `null`, never an exception — it is up to the caller to decide whether + * the absence of a format is an error. */ interface FormatDetectorInterface { /** - * @param string $path chemin absolu du fichier à identifier + * @param string $path absolute path of the file to identify * - * @return Format|null le format détecté, ou null si le fichier n'est pas - * un RAW supporté, est illisible ou n'existe pas + * @return Format|null the detected format, or null if the file is not a + * supported RAW, is unreadable or does not exist */ public function detect(string $path): ?Format; } diff --git a/src/Parser/Cr3/Box.php b/src/Parser/Cr3/Box.php index 8c5872f..9b3d58e 100644 --- a/src/Parser/Cr3/Box.php +++ b/src/Parser/Cr3/Box.php @@ -5,19 +5,19 @@ namespace RonanLenouvel\RawPreviewExtractor\Parser\Cr3; /** - * Une boîte ISO-BMFF, localisée dans le fichier. + * An ISO-BMFF box, located in the file. * - * La boîte porte sa position et sa taille, jamais son contenu : un `mdat` pèse - * plusieurs dizaines de mégaoctets, on ne le charge pas pour l'inventorier. - * C'est {@see IsoBmffBoxReader::readPayload()} qui lit les octets à la demande. + * The box carries its position and its size, never its content: an `mdat` + * weighs several tens of megabytes, we do not load it just to inventory it. + * It is {@see IsoBmffBoxReader::readPayload()} that reads the bytes on demand. */ final readonly class Box { /** - * @param string $type type sur 4 caractères ASCII (`ftyp`, `moov`, `uuid`…) - * @param int $offset position de la boîte dans le fichier, en-tête compris - * @param int $payloadOffset position du contenu, après l'en-tête - * @param int $payloadLength taille du contenu, en octets + * @param string $type 4-ASCII-character type (`ftyp`, `moov`, `uuid`…) + * @param int $offset position of the box in the file, header included + * @param int $payloadOffset position of the content, after the header + * @param int $payloadLength size of the content, in bytes */ public function __construct( public string $type, diff --git a/src/Parser/Cr3/Cr3PreviewParser.php b/src/Parser/Cr3/Cr3PreviewParser.php index 8c506e0..2d22358 100644 --- a/src/Parser/Cr3/Cr3PreviewParser.php +++ b/src/Parser/Cr3/Cr3PreviewParser.php @@ -11,51 +11,51 @@ use RonanLenouvel\RawPreviewExtractor\Parser\PreviewParserInterface; /** - * Extrait la preview JPEG d'un CR3 (Canon RAW v3, conteneur ISO-BMFF). + * Extracts the JPEG preview of a CR3 (Canon RAW v3, ISO-BMFF container). * - * La preview vit dans la boîte `PRVW`, sous une boîte `uuid` dédiée **à la racine** - * du fichier. `THMB` porte une vignette bien plus petite, sous l'UUID Canon, et - * sert de repli. + * The preview lives in the `PRVW` box, under a dedicated `uuid` box **at the + * root** of the file. `THMB` carries a much smaller thumbnail, under the Canon + * UUID, and serves as a fallback. * - * **Le CR3 n'a pas de spécification publique** : sa structure vient de - * rétro-ingénierie communautaire et peut varier selon les modèles. Le code est - * donc écrit pour être tolérant — il localise le JPEG par son magic plutôt que - * de s'appuyer sur des décalages fixes. + * **CR3 has no public specification**: its structure comes from community + * reverse engineering and may vary depending on the model. The code is + * therefore written to be tolerant — it locates the JPEG by its magic rather + * than relying on fixed offsets. * - * Ce parseur orchestre : il ne lit pas d'octets lui-même, il délègue à + * This parser orchestrates: it does not read bytes itself, it delegates to * {@see IsoBmffBoxReader}. */ final class Cr3PreviewParser implements PreviewParserInterface { /** - * Emplacements des previews, par ordre de préférence. + * Preview locations, in order of preference. * - * `PRVW` et `THMB` ne vivent **ni sous le même UUID, ni au même niveau** — - * structure vérifiée sur Canon EOS R et EOS RP : + * `PRVW` and `THMB` live **neither under the same UUID, nor at the same + * level** — structure verified on Canon EOS R and EOS RP: * * ``` * ftyp * moov - * └── uuid 85c0b687… → CMT1, CMT2, THMB (vignette ~15 Ko) - * uuid eaf42b5e… → PRVW (preview ~250 Ko) + * └── uuid 85c0b687… → CMT1, CMT2, THMB (thumbnail ~15 KB) + * uuid eaf42b5e… → PRVW (preview ~250 KB) * mdat * ``` * - * Chercher les deux sous l'UUID Canon ne trouve que la vignette. + * Looking for both under the Canon UUID only finds the thumbnail. * * @var list */ private const PREVIEW_LOCATIONS = [ - // La vraie preview, dans sa propre boîte uuid à la racine. + // The real preview, in its own uuid box at the root. ['uuid' => 'eaf42b5e1c984b88b9fbb7dc406e4d16', 'box' => 'PRVW'], - // Repli : la vignette de l'UUID Canon, sous moov. + // Fallback: the thumbnail of the Canon UUID, under moov. ['uuid' => '85c0b687820f11e08111f4ce462b6a48', 'box' => 'THMB'], ]; - /** Marqueur de début de tout JPEG (SOI). */ + /** Start marker of every JPEG (SOI). */ private const JPEG_MAGIC = "\xFF\xD8"; - /** Longueur de l'UUID qui suit le type d'une boîte `uuid`. */ + /** Length of the UUID that follows the type of a `uuid` box. */ private const UUID_LENGTH = 16; public function extract(string $path, Format $format): ExtractedPreview @@ -79,20 +79,20 @@ public function extract(string $path, Format $format): ExtractedPreview } throw new PreviewNotFoundException(sprintf( - 'Aucune preview JPEG dans %s : ni PRVW ni THMB exploitable.', + 'No JPEG preview in %s: neither PRVW nor THMB usable.', basename($path), )); } /** - * Extrait le JPEG d'une boîte de preview, s'il s'y trouve. + * Extracts the JPEG from a preview box, if it is there. * - * @throws CorruptedFileException si la structure est invalide + * @throws CorruptedFileException if the structure is invalid */ private function jpegFromBox(IsoBmffBoxReader $reader, Box $container, string $type): ?string { - // Certains conteneurs uuid commencent par des octets propriétaires : - // le parcours normal échoue alors, on retombe sur une recherche par type. + // Some uuid containers start with proprietary bytes: the normal walk + // then fails, and we fall back on a search by type. $box = $this->findWithin($reader, $container, $type) ?? $this->findByScanning($reader, $container, $type); @@ -102,23 +102,23 @@ private function jpegFromBox(IsoBmffBoxReader $reader, Box $container, string $t $payload = $reader->readPayload($box); - // PRVW précède son JPEG d'un en-tête propriétaire dont la taille varie - // selon les modèles et n'est pas documentée. Chercher le magic est plus - // robuste que de coder un décalage en dur — et le magic est à valider - // de toute façon. + // PRVW precedes its JPEG with a proprietary header whose size varies + // between models and is not documented. Looking for the magic is more + // robust than hard-coding an offset — and the magic has to be validated + // anyway. $start = strpos($payload, self::JPEG_MAGIC); return false === $start ? null : substr($payload, $start); } /** - * Cherche une boîte parmi les filles directes d'un conteneur uuid. + * Looks for a box among the direct children of a uuid container. * - * On ne cherche pas dans tout le fichier : une boîte `PRVW` ailleurs - * n'est pas la preview du CR3, et s'y fier reviendrait à faire confiance - * à n'importe quelle boîte portant le bon nom. + * We do not search the whole file: a `PRVW` box elsewhere is not the CR3's + * preview, and relying on it would amount to trusting any box carrying the + * right name. * - * @throws CorruptedFileException si la structure est invalide + * @throws CorruptedFileException if the structure is invalid */ private function findWithin(IsoBmffBoxReader $reader, Box $container, string $type): ?Box { @@ -132,23 +132,23 @@ private function findWithin(IsoBmffBoxReader $reader, Box $container, string $ty } /** - * Cherche une boîte dans le contenu brut d'un conteneur, sans supposer que - * celui-ci commence par une boîte. + * Looks for a box in the raw content of a container, without assuming that + * the latter starts with a box. * - * La boîte `uuid` qui porte `PRVW` insère **8 octets propriétaires** entre - * l'UUID et la première boîte — vérifié sur EOS R et EOS RP. Sa taille n'est - * documentée nulle part, et rien ne garantit qu'elle soit la même partout. - * On localise donc le type recherché dans le contenu, plutôt que de coder un - * décalage en dur. + * The `uuid` box carrying `PRVW` inserts **8 proprietary bytes** between the + * UUID and the first box — verified on EOS R and EOS RP. Its size is + * documented nowhere, and nothing guarantees it is the same everywhere. We + * therefore locate the sought type within the content, rather than + * hard-coding an offset. * - * @throws CorruptedFileException si la structure est invalide + * @throws CorruptedFileException if the structure is invalid */ private function findByScanning(IsoBmffBoxReader $reader, Box $container, string $type): ?Box { $payload = $reader->readPayload($container); $position = strpos($payload, $type, self::UUID_LENGTH); - // Le type est précédé des 4 octets de taille : la boîte commence là. + // The type is preceded by the 4 size bytes: the box starts there. if (false === $position || $position < 4) { return null; } @@ -164,11 +164,11 @@ private function findByScanning(IsoBmffBoxReader $reader, Box $container, string } /** - * Lit les dimensions dans le segment SOF du JPEG. + * Reads the dimensions from the JPEG's SOF segment. * * @return array{int, int} * - * @throws CorruptedFileException si aucun segment SOF n'est trouvable + * @throws CorruptedFileException if no SOF segment can be found */ private function readJpegDimensions(string $jpeg): array { @@ -184,7 +184,7 @@ private function readJpegDimensions(string $jpeg): array $marker = ord($jpeg[$position + 1]); - // SOF0 à SOF15, hors DHT (C4), DNL (C8) et DAC (CC) qui partagent la plage. + // SOF0 to SOF15, except DHT (C4), DNL (C8) and DAC (CC) which share the range. if ($marker >= 0xC0 && $marker <= 0xCF && !in_array($marker, [0xC4, 0xC8, 0xCC], true)) { return [ unpack('n', substr($jpeg, $position + 7, 2))[1], @@ -196,7 +196,7 @@ private function readJpegDimensions(string $jpeg): array } throw new CorruptedFileException( - 'JPEG sans segment SOF : dimensions introuvables.', + 'JPEG without SOF segment: dimensions not found.', ); } } diff --git a/src/Parser/Cr3/IsoBmffBoxReader.php b/src/Parser/Cr3/IsoBmffBoxReader.php index 2ca660b..07e8de6 100644 --- a/src/Parser/Cr3/IsoBmffBoxReader.php +++ b/src/Parser/Cr3/IsoBmffBoxReader.php @@ -7,37 +7,37 @@ use RonanLenouvel\RawPreviewExtractor\Exception\CorruptedFileException; /** - * Lecture bas niveau d'un conteneur ISO-BMFF (famille MP4/HEIF, dont le CR3). + * Low-level reading of an ISO-BMFF container (MP4/HEIF family, CR3 included). * - * Ce lecteur connaît la **structure** — un arbre de boîtes `[taille][type][contenu]` — - * et rien de la sémantique : il ignore ce qu'est une preview. + * This reader knows the **structure** — a tree of `[size][type][content]` boxes — + * and nothing of the semantics: it has no idea what a preview is. * - * **ISO-BMFF est toujours big-endian**, quel que soit l'appareil : contrairement - * au TIFF, il n'y a pas d'ordre d'octets à détecter. + * **ISO-BMFF is always big-endian**, whatever the camera: unlike TIFF, there is + * no byte order to detect. * - * Le fichier est traité comme de l'entrée **non fiable** : chaque taille est - * validée contre la taille réelle du fichier, la progression du curseur est - * vérifiée à chaque itération, et la récursion est bornée. + * The file is treated as **untrusted** input: every size is validated against + * the file's real size, the cursor's progress is checked at each iteration, and + * the recursion is bounded. */ final class IsoBmffBoxReader { - /** Taille d'un en-tête de boîte : 4 octets de taille + 4 de type. */ + /** Size of a box header: 4 bytes of size + 4 of type. */ private const HEADER_LENGTH = 8; - /** En-tête étendu, quand la taille est portée sur 64 bits. */ + /** Extended header, when the size is carried on 64 bits. */ private const EXTENDED_HEADER_LENGTH = 16; - /** Longueur de l'UUID qui suit le type d'une boîte `uuid`. */ + /** Length of the UUID that follows the type of a `uuid` box. */ private const UUID_LENGTH = 16; - /** Profondeur maximale d'imbrication : un fichier hostile pourrait saturer la pile. */ + /** Maximum nesting depth: a hostile file could saturate the stack. */ private const MAX_DEPTH = 8; /** - * Boîtes dont le contenu est lui-même un arbre de boîtes. + * Boxes whose content is itself a tree of boxes. * - * Toute autre boîte est une feuille : `mdat` contient des données brutes que - * l'on prendrait pour des boîtes si on descendait dedans. + * Any other box is a leaf: `mdat` contains raw data that we would mistake + * for boxes if we descended into it. */ private const CONTAINER_TYPES = ['moov', 'trak', 'mdia', 'minf', 'stbl', 'uuid']; @@ -47,14 +47,14 @@ final class IsoBmffBoxReader private readonly int $fileSize; /** - * @param string $path chemin du fichier à lire + * @param string $path path of the file to read * - * @throws CorruptedFileException si le fichier est illisible + * @throws CorruptedFileException if the file is unreadable */ public function __construct(private readonly string $path) { if (!is_file($path)) { - throw new CorruptedFileException(sprintf('Fichier illisible : %s', $path)); + throw new CorruptedFileException(sprintf('Unreadable file: %s', $path)); } $this->handle = fopen($path, 'rb'); @@ -67,20 +67,20 @@ public function __destruct() } /** - * Inventorie les boîtes de premier niveau. + * Inventories the top-level boxes. * * @return list * - * @throws CorruptedFileException si une taille est invalide ou hors bornes + * @throws CorruptedFileException if a size is invalid or out of bounds */ public function readBoxes(): array { - // Un fichier trop court pour porter ne serait-ce qu'un en-tête n'est pas - // un conteneur vide : il est tronqué. À l'intérieur d'une boîte en - // revanche, un reliquat de moins de 8 octets est du padding normal. + // A file too short to carry even a header is not an empty container: it + // is truncated. Inside a box on the other hand, a remainder of less than + // 8 bytes is normal padding. if ($this->fileSize < self::HEADER_LENGTH) { throw new CorruptedFileException(sprintf( - 'Fichier tronqué : %d octets, moins qu\'un en-tête de boîte.', + 'Truncated file: %d bytes, less than a box header.', $this->fileSize, )); } @@ -89,11 +89,11 @@ public function readBoxes(): array } /** - * Cherche la première boîte du type donné, en descendant dans les conteneurs. + * Looks for the first box of the given type, descending into the containers. * - * @param string $type type sur 4 caractères + * @param string $type 4-character type * - * @throws CorruptedFileException si la structure est invalide + * @throws CorruptedFileException if the structure is invalid */ public function find(string $type): ?Box { @@ -101,14 +101,14 @@ public function find(string $type): ?Box } /** - * Cherche une boîte `uuid` portant l'UUID donné. + * Looks for a `uuid` box carrying the given UUID. * - * Plusieurs boîtes `uuid` distinctes coexistent dans un CR3 : le type ne - * suffit pas à les identifier, il faut lire les 16 octets qui le suivent. + * Several distinct `uuid` boxes coexist in a CR3: the type is not enough to + * identify them, the 16 bytes that follow it must be read. * - * @param string $uuid les 16 octets bruts de l'UUID recherché + * @param string $uuid the 16 raw bytes of the UUID being looked for * - * @throws CorruptedFileException si la structure est invalide + * @throws CorruptedFileException if the structure is invalid */ public function findUuid(string $uuid): ?Box { @@ -116,11 +116,11 @@ public function findUuid(string $uuid): ?Box } /** - * Inventorie les boîtes filles d'un conteneur. + * Inventories the child boxes of a container. * * @return list * - * @throws CorruptedFileException si la structure est invalide + * @throws CorruptedFileException if the structure is invalid */ public function childBoxes(Box $box): array { @@ -128,9 +128,9 @@ public function childBoxes(Box $box): array } /** - * Lit le contenu d'une boîte. + * Reads the content of a box. * - * @throws CorruptedFileException si la plage sort du fichier + * @throws CorruptedFileException if the range falls outside the file */ public function readPayload(Box $box): string { @@ -138,15 +138,15 @@ public function readPayload(Box $box): string } /** - * Lit `$length` octets bruts à partir de `$offset`. + * Reads `$length` raw bytes starting from `$offset`. * - * @throws CorruptedFileException si la plage sort du fichier + * @throws CorruptedFileException if the range falls outside the file */ public function readBytes(int $offset, int $length): string { if ($length < 1 || $offset < 0 || $offset + $length > $this->fileSize) { throw new CorruptedFileException(sprintf( - 'Lecture hors bornes : %d octets à l\'offset %d (taille du fichier : %d).', + 'Read out of bounds: %d bytes at offset %d (file size: %d).', $length, $offset, $this->fileSize, @@ -159,7 +159,7 @@ public function readBytes(int $offset, int $length): string } /** - * Inventorie les boîtes contenues dans une plage donnée. + * Inventories the boxes contained in a given range. * * @return list * @@ -174,9 +174,9 @@ private function readBoxesIn(int $start, int $end): array $box = $this->readBoxAt($offset, $end); $boxes[] = $box; - // La progression est structurellement garantie : readBoxAt() refuse - // toute taille inférieure à l'en-tête, donc le curseur avance d'au - // moins 8 octets à chaque tour. + // Progress is structurally guaranteed: readBoxAt() refuses any size + // smaller than the header, so the cursor advances by at least + // 8 bytes on each pass. $offset = $box->payloadOffset + $box->payloadLength; } @@ -184,7 +184,7 @@ private function readBoxesIn(int $start, int $end): array } /** - * Décode l'en-tête d'une boîte, en traitant les trois cas particuliers de `size`. + * Decodes a box header, handling the three special cases of `size`. * * @throws CorruptedFileException */ @@ -194,13 +194,13 @@ private function readBoxAt(int $offset, int $end): Box $size = unpack('N', substr($header, 0, 4))[1]; $type = substr($header, 4, 4); - // size == 1 : la taille réelle est sur 64 bits, juste après le type. + // size == 1: the real size is on 64 bits, right after the type. if (1 === $size) { $size = unpack('J', $this->readBytes($offset + self::HEADER_LENGTH, 8))[1]; if ($size < self::EXTENDED_HEADER_LENGTH) { throw new CorruptedFileException(sprintf( - 'Taille de boîte 64 bits invalide : %d à l\'offset %d.', + 'Invalid 64-bit box size: %d at offset %d.', $size, $offset, )); @@ -209,14 +209,14 @@ private function readBoxAt(int $offset, int $end): Box return $this->box($type, $offset, self::EXTENDED_HEADER_LENGTH, $size, $end); } - // size == 0 : la boîte s'étend jusqu'à la fin du fichier. + // size == 0: the box extends to the end of the file. if (0 === $size) { return $this->box($type, $offset, self::HEADER_LENGTH, $end - $offset, $end); } if ($size < self::HEADER_LENGTH) { throw new CorruptedFileException(sprintf( - 'Taille de boîte invalide : %d à l\'offset %d (minimum 8).', + 'Invalid box size: %d at offset %d (minimum 8).', $size, $offset, )); @@ -226,13 +226,13 @@ private function readBoxAt(int $offset, int $end): Box } /** - * @throws CorruptedFileException si la boîte déborde de la plage autorisée + * @throws CorruptedFileException if the box overflows the allowed range */ private function box(string $type, int $offset, int $headerLength, int $size, int $end): Box { if ($offset + $size > $end) { throw new CorruptedFileException(sprintf( - 'Boîte « %s » hors bornes : %d octets annoncés à l\'offset %d.', + 'Box "%s" out of bounds: %d bytes announced at offset %d.', $type, $size, $offset, @@ -243,10 +243,10 @@ private function box(string $type, int $offset, int $headerLength, int $size, in } /** - * Cherche récursivement une boîte `uuid` portant l'UUID donné. + * Recursively looks for a `uuid` box carrying the given UUID. * - * Dans un CR3 réel, l'UUID Canon vit sous `moov` et non à la racine : une - * recherche limitée au premier niveau ne le trouverait jamais. + * In a real CR3, the Canon UUID lives under `moov` and not at the root: a + * search limited to the first level would never find it. * * @param list $boxes * @@ -311,7 +311,7 @@ private function findIn(array $boxes, string $type, int $depth): ?Box } /** - * Boîtes filles d'un conteneur. + * Child boxes of a container. * * @return list * @@ -319,7 +319,7 @@ private function findIn(array $boxes, string $type, int $depth): ?Box */ private function childrenOf(Box $box): array { - // Une boîte uuid porte 16 octets d'UUID avant son contenu. + // A uuid box carries 16 bytes of UUID before its content. $start = 'uuid' === $box->type ? $box->payloadOffset + self::UUID_LENGTH : $box->payloadOffset; @@ -333,14 +333,14 @@ private function childrenOf(Box $box): array try { return $this->readBoxesIn($start, $end); } catch (CorruptedFileException) { - // Descendre dans un conteneur est spéculatif : toutes les boîtes - // `uuid` ne contiennent pas des boîtes. Celle qui porte PRVW dans un - // CR3 commence par un en-tête propriétaire, dont les octets lus - // comme un en-tête donnent une taille absurde. + // Descending into a container is speculative: not every `uuid` box + // contains boxes. The one carrying PRVW in a CR3 starts with a + // proprietary header, whose bytes read as a header give an absurd + // size. // - // Une boîte sans enfants lisibles n'a pas d'enfants — ce n'est pas - // une corruption du fichier. Les lectures non spéculatives - // (readBoxes, readPayload, readBytes) restent strictes. + // A box without readable children has no children — this is not a + // corruption of the file. The non-speculative reads (readBoxes, + // readPayload, readBytes) stay strict. return []; } } diff --git a/src/Parser/PreviewParserInterface.php b/src/Parser/PreviewParserInterface.php index fd2e3c7..4ea1876 100644 --- a/src/Parser/PreviewParserInterface.php +++ b/src/Parser/PreviewParserInterface.php @@ -10,21 +10,21 @@ use RonanLenouvel\RawPreviewExtractor\Format\Format; /** - * Extrait la preview JPEG d'un fichier RAW d'un conteneur donné. + * Extracts the JPEG preview of a RAW file of a given container. * - * Une implémentation par famille de conteneur : TIFF pour CR2/NEF/ARW/DNG, - * ISO-BMFF pour CR3. Toutes sont interchangeables derrière ce contrat — c'est - * ce qui permet à la façade de résoudre par une simple map `Format → parser`, - * et d'accueillir un nouveau format sans être modifiée. + * One implementation per container family: TIFF for CR2/NEF/ARW/DNG, ISO-BMFF + * for CR3. All are interchangeable behind this contract — that is what lets the + * facade resolve through a simple `Format → parser` map, and welcome a new + * format without being modified. */ interface PreviewParserInterface { /** - * @param string $path chemin du fichier RAW - * @param Format $format format déjà identifié par le détecteur + * @param string $path path of the RAW file + * @param Format $format format already identified by the detector * - * @throws PreviewNotFoundException si le fichier est valide mais sans preview - * @throws CorruptedFileException si le fichier est illisible ou incohérent + * @throws PreviewNotFoundException if the file is valid but without a preview + * @throws CorruptedFileException if the file is unreadable or inconsistent */ public function extract(string $path, Format $format): ExtractedPreview; } diff --git a/src/Parser/Tiff/IfdEntry.php b/src/Parser/Tiff/IfdEntry.php index 8365fa1..739d043 100644 --- a/src/Parser/Tiff/IfdEntry.php +++ b/src/Parser/Tiff/IfdEntry.php @@ -5,25 +5,25 @@ namespace RonanLenouvel\RawPreviewExtractor\Parser\Tiff; /** - * Une entrée d'IFD, valeurs déjà résolues. + * An IFD entry, values already resolved. * - * Value object pur : il ne connaît ni fichier, ni handle, ni endianness. C'est - * le {@see TiffReader} qui résout les valeurs à la lecture — y compris celles - * stockées hors de l'entrée — et construit des `IfdEntry` complètes. + * A pure value object: it knows neither file, nor handle, nor endianness. It is + * the {@see TiffReader} that resolves the values while reading — including + * those stored outside the entry — and builds complete `IfdEntry` objects. * - * Cette entrée est donc librement transportable et comparable, ce qu'un objet - * qui garderait un curseur de fichier ne serait pas. + * This entry is therefore freely transportable and comparable, which an object + * holding on to a file cursor would not be. */ final readonly class IfdEntry { /** - * @param int $tag identifiant du tag (voir {@see TiffTag}) - * @param int $type code du type de donnée TIFF (1 à 12) - * @param int $count nombre de valeurs, tel qu'annoncé par l'entrée - * @param list $values valeurs numériques résolues ; vide si le type - * est ASCII ou inconnu - * @param string $ascii valeur textuelle, NUL de fin retiré ; chaîne - * vide si le type n'est pas ASCII + * @param int $tag tag identifier (see {@see TiffTag}) + * @param int $type TIFF data type code (1 to 12) + * @param int $count number of values, as announced by the entry + * @param list $values resolved numeric values; empty if the type is + * ASCII or unknown + * @param string $ascii textual value, trailing NUL removed; empty + * string if the type is not ASCII */ public function __construct( public int $tag, @@ -35,10 +35,10 @@ public function __construct( } /** - * Première valeur numérique de l'entrée, ou null s'il n'y en a pas. + * First numeric value of the entry, or null if there is none. * - * Raccourci pour le cas courant : la plupart des tags exploités ici - * (offsets, tailles, dimensions) portent une valeur unique. + * A shortcut for the common case: most tags used here (offsets, sizes, + * dimensions) carry a single value. */ public function value(): ?int { diff --git a/src/Parser/Tiff/TiffPreviewParser.php b/src/Parser/Tiff/TiffPreviewParser.php index a63d4ef..ccffedd 100644 --- a/src/Parser/Tiff/TiffPreviewParser.php +++ b/src/Parser/Tiff/TiffPreviewParser.php @@ -11,44 +11,44 @@ use RonanLenouvel\RawPreviewExtractor\Parser\PreviewParserInterface; /** - * Extrait la preview JPEG des RAW bâtis sur TIFF : CR2, NEF, ARW et DNG. + * Extracts the JPEG preview of RAW files built on TIFF: CR2, NEF, ARW and DNG. * - * Un seul parseur couvre les quatre formats. Plutôt que de coder en dur - * l'organisation d'un constructeur — variable d'une génération d'appareil à - * l'autre — il parcourt **tous** les IFD, collecte tous les blocs JPEG - * candidats et retient **le plus grand**. + * A single parser covers the four formats. Rather than hard-coding a + * manufacturer's layout — which varies from one camera generation to the next — + * it walks **all** the IFDs, collects every candidate JPEG block and keeps + * **the largest**. * - * Ce parseur orchestre : il ne lit pas d'octets lui-même, il délègue au + * This parser orchestrates: it does not read bytes itself, it delegates to the * {@see TiffReader}. */ final class TiffPreviewParser implements PreviewParserInterface { - /** Compression JPEG « ancienne » et JPEG au sens TIFF 6.0. */ + /** "Old-style" JPEG compression and JPEG in the TIFF 6.0 sense. */ private const JPEG_COMPRESSIONS = [6, 7]; - /** Tout JPEG commence par ce marqueur (SOI). */ + /** Every JPEG starts with this marker (SOI). */ private const JPEG_MAGIC = "\xFF\xD8"; /** - * Marqueurs SOF d'un JPEG réellement décodable. + * SOF markers of a genuinely decodable JPEG. * - * `Compression = 6` ne suffit pas à distinguer une preview des données du - * capteur : Canon stocke celles-ci en **JPEG lossless** dans un CR2, avec le - * même tag. Ces blocs sont les plus gros du fichier et gagneraient donc la - * comparaison par taille — pour rendre 28 Mo qu'aucun décodeur ne lit. + * `Compression = 6` is not enough to tell a preview from the sensor data: + * Canon stores the latter as **lossless JPEG** in a CR2, with the same tag. + * Those blocks are the biggest in the file and would therefore win the + * comparison by size — only to return 28 MB that no decoder reads. * - * Vérifié sur six appareils : les previews utilisent toutes SOF0 ; seul le - * capteur d'un CR2 utilise SOF3. + * Verified on six cameras: the previews all use SOF0; only a CR2's sensor + * uses SOF3. * * @var list */ private const DECODABLE_SOF_MARKERS = [ - 0xC0, // SOF0 — baseline, le cas de toutes les previews observées + 0xC0, // SOF0 — baseline, the case of every observed preview 0xC1, // SOF1 — extended sequential - 0xC2, // SOF2 — progressif + 0xC2, // SOF2 — progressive ]; - /** Profondeur maximale de récursion dans les sous-IFD. */ + /** Maximum recursion depth into the SubIFDs. */ private const MAX_SUB_IFD_DEPTH = 4; public function extract(string $path, Format $format): ExtractedPreview @@ -60,12 +60,12 @@ public function extract(string $path, Format $format): ExtractedPreview $this->collectFromIfd($reader, $offset, $candidates, 0); } - // La plus grande preview est la plus utile : un RAW en porte souvent - // plusieurs, de la vignette 160×120 à la pleine résolution. + // The largest preview is the most useful: a RAW often carries several, + // from the 160x120 thumbnail to full resolution. usort($candidates, static fn (array $a, array $b): int => $b['length'] <=> $a['length']); - // Un candidat n'est retenu que s'il est réellement décodable : le plus - // gros bloc d'un CR2 est le capteur en JPEG lossless, pas une preview. + // A candidate is only kept if it is genuinely decodable: the biggest + // block of a CR2 is the sensor in lossless JPEG, not a preview. foreach ($candidates as $candidate) { $preview = $this->tryBuildPreview($reader, $candidate, $format); @@ -75,12 +75,12 @@ public function extract(string $path, Format $format): ExtractedPreview } throw new PreviewNotFoundException( - sprintf('Aucune preview JPEG exploitable dans %s.', basename($path)), + sprintf('No usable JPEG preview in %s.', basename($path)), ); } /** - * Collecte les candidats d'un IFD, puis descend dans ses sous-IFD. + * Collects the candidates of an IFD, then descends into its SubIFDs. * * @param list $candidates * @@ -107,7 +107,7 @@ private function collectFromIfd(TiffReader $reader, int $offset, array &$candida } /** - * Le chemin courant : le couple JPEGInterchangeFormat / …Length. + * The common path: the JPEGInterchangeFormat / …Length pair. * * @param array $entries * @@ -126,9 +126,9 @@ private function jpegInterchangeCandidate(array $entries): ?array } /** - * L'autre chemin : StripOffsets / StripByteCounts, si et seulement si le - * tag Compression annonce du JPEG. Sans ce contrôle, on prendrait les - * données brutes du capteur pour une preview. + * The other path: StripOffsets / StripByteCounts, if and only if the + * Compression tag announces JPEG. Without this check, we would mistake the + * raw sensor data for a preview. * * @param array $entries * @@ -183,19 +183,19 @@ private function tryBuildPreview(TiffReader $reader, array $candidate, Format $f { $jpeg = $reader->readBytes($candidate['offset'], $candidate['length']); - // Un tag qui ment sur son contenu est courant dans les RAW : le - // PowerShot G12 annonce Compression = 6 sur des données brutes, dans - // l'IFD dont le bloc est justement le plus gros. Ce n'est pas une - // corruption du fichier — c'est un candidat de plus à écarter. + // A tag that lies about its content is common in RAW files: the + // PowerShot G12 announces Compression = 6 on raw data, in the IFD whose + // block happens to be the biggest. This is not a corruption of the file + // — it is one more candidate to rule out. if (!str_starts_with($jpeg, self::JPEG_MAGIC)) { return null; } $sof = $this->findSofSegment($jpeg); - // Pas de SOF, ou un SOF que personne ne décode (lossless, arithmétique, - // différentiel) : ce bloc n'est pas une preview affichable. On passe au - // candidat suivant plutôt que de rendre des octets inutilisables. + // No SOF, or a SOF that nobody decodes (lossless, arithmetic, + // differential): this block is not a displayable preview. We move on to + // the next candidate rather than return unusable bytes. if (null === $sof || !in_array($sof['marker'], self::DECODABLE_SOF_MARKERS, true)) { return null; } @@ -204,15 +204,15 @@ private function tryBuildPreview(TiffReader $reader, array $candidate, Format $f } /** - * Localise le segment SOF et en extrait le marqueur et les dimensions. + * Locates the SOF segment and extracts its marker and dimensions from it. * - * Le SOF fait autorité sur les dimensions : les tags ImageWidth/ImageLength - * de l'IFD décrivent souvent l'image RAW pleine résolution, pas la preview. - * Son marqueur dit aussi **comment** l'image est encodée, donc si un - * décodeur courant saura la lire. + * The SOF is authoritative on the dimensions: the IFD's ImageWidth/ImageLength + * tags often describe the full-resolution RAW image, not the preview. Its + * marker also says **how** the image is encoded, hence whether a common + * decoder will know how to read it. * - * @return array{marker: int, width: int, height: int}|null null si le JPEG - * ne porte aucun SOF + * @return array{marker: int, width: int, height: int}|null null if the JPEG + * carries no SOF */ private function findSofSegment(string $jpeg): ?array { @@ -228,7 +228,7 @@ private function findSofSegment(string $jpeg): ?array $marker = ord($jpeg[$position + 1]); - // SOF0 à SOF15, hors DHT (C4), DNL (C8) et DAC (CC) qui partagent la plage. + // SOF0 to SOF15, except DHT (C4), DNL (C8) and DAC (CC) which share the range. if ($marker >= 0xC0 && $marker <= 0xCF && !in_array($marker, [0xC4, 0xC8, 0xCC], true)) { return [ 'marker' => $marker, diff --git a/src/Parser/Tiff/TiffReader.php b/src/Parser/Tiff/TiffReader.php index 567e473..486c783 100644 --- a/src/Parser/Tiff/TiffReader.php +++ b/src/Parser/Tiff/TiffReader.php @@ -7,15 +7,15 @@ use RonanLenouvel\RawPreviewExtractor\Exception\CorruptedFileException; /** - * Lecture bas niveau d'un conteneur TIFF 6.0. + * Low-level reading of a TIFF 6.0 container. * - * Ce lecteur connaît la **structure** du conteneur — en-tête, chaîne d'IFD, - * entrées typées — et rien de la sémantique du contenu : il ignore ce qu'est - * une preview. C'est cette frontière qui le rend testable sans fichier RAW réel. + * This reader knows the container's **structure** — header, IFD chain, typed + * entries — and nothing of the content's semantics: it has no idea what a + * preview is. It is this boundary that makes it testable without a real RAW file. * - * Toutes les lectures traitent le fichier comme de l'entrée **non fiable** : - * chaque offset est validé contre la taille réelle, chaque `fread` est vérifié - * en longueur avant `unpack`, et la chaîne d'IFD est protégée contre les boucles. + * All reads treat the file as **untrusted** input: every offset is validated + * against the real size, every `fread` is checked for length before `unpack`, + * and the IFD chain is protected against loops. */ final class TiffReader { @@ -23,26 +23,26 @@ final class TiffReader private const TIFF_MAGIC = 42; private const ENTRY_LENGTH = 12; - /** Au-delà, un IFD trahit un fichier corrompu ou hostile. */ + /** Beyond this, an IFD betrays a corrupted or hostile file. */ private const MAX_ENTRIES_PER_IFD = 4096; - /** Garde-fou contre une chaîne d'IFD artificiellement longue. */ + /** Safeguard against an artificially long IFD chain. */ private const MAX_IFD_CHAIN = 64; /** - * Au-delà, une valeur n'est pas résolue — l'entrée reste lisible. + * Beyond this, a value is not resolved — the entry stays readable. * - * Aucun tag exploité par ce package n'approche cette taille : ce sont des - * offsets, des tailles, des dimensions, un nom de fabricant. Les gros blocs - * sont des métadonnées propriétaires (`MakerNote` pèse couramment 75 Ko dans - * un CR2) que l'on traverse sans jamais les lire. + * No tag used by this package comes close to this size: they are offsets, + * sizes, dimensions, a manufacturer name. The big blocks are proprietary + * metadata (`MakerNote` commonly weighs 75 KB in a CR2) that we traverse + * without ever reading them. * - * Ne pas les résoudre évite deux choses : gaspiller la lecture, et rejeter - * un fichier parfaitement valide — ce qui arrivait au Canon 5D de 2005. + * Not resolving them avoids two things: wasting the read, and rejecting a + * perfectly valid file — which is what happened to the 2005 Canon 5D. */ private const MAX_RESOLVED_VALUE_LENGTH = 65536; - /** Taille en octets de chaque type TIFF 6.0, indexée par code de type. */ + /** Size in bytes of each TIFF 6.0 type, indexed by type code. */ private const TYPE_SIZES = [ 1 => 1, // BYTE 2 => 1, // ASCII @@ -66,33 +66,33 @@ final class TiffReader private readonly int $firstIfdOffset; /** - * @param string $path chemin du fichier TIFF à lire + * @param string $path path of the TIFF file to read * - * @throws CorruptedFileException si le fichier est illisible ou n'est pas un TIFF + * @throws CorruptedFileException if the file is unreadable or is not a TIFF */ public function __construct(private readonly string $path) { if (!is_file($path)) { - throw new CorruptedFileException(sprintf('Fichier illisible : %s', $path)); + throw new CorruptedFileException(sprintf('Unreadable file: %s', $path)); } - // is_file() a déjà écarté l'absent et le répertoire : fopen ne peut - // plus échouer que sur un problème de droits, que le @ absorberait - // silencieusement — on le laisse donc remonter. + // is_file() has already ruled out the missing file and the directory: + // fopen can now only fail on a permissions problem, which @ would + // absorb silently — so we let it bubble up. $this->handle = fopen($path, 'rb'); $this->fileSize = (int) filesize($path); $header = (string) fread($this->handle, self::HEADER_LENGTH); if (self::HEADER_LENGTH !== strlen($header)) { - throw new CorruptedFileException('En-tête TIFF tronqué : moins de 8 octets.'); + throw new CorruptedFileException('Truncated TIFF header: fewer than 8 bytes.'); } $this->bigEndian = match (substr($header, 0, 2)) { 'II' => false, 'MM' => true, default => throw new CorruptedFileException( - 'Ordre d\'octets inconnu : ni « II » ni « MM ».', + 'Unknown byte order: neither "II" nor "MM".', ), }; @@ -100,7 +100,7 @@ public function __construct(private readonly string $path) if (self::TIFF_MAGIC !== $magic) { throw new CorruptedFileException( - sprintf('Nombre magique TIFF invalide : %d au lieu de 42.', $magic), + sprintf('Invalid TIFF magic number: %d instead of 42.', $magic), ); } @@ -113,7 +113,7 @@ public function __destruct() } /** - * Le fichier est-il big-endian (« MM ») ? + * Is the file big-endian ("MM")? */ public function isBigEndian(): bool { @@ -121,15 +121,15 @@ public function isBigEndian(): bool } /** - * Parcourt la chaîne d'IFD et renvoie les offsets rencontrés, dans l'ordre. + * Walks the IFD chain and returns the offsets encountered, in order. * - * Un IFD déjà visité clôt le parcours : c'est le garde-fou anti-boucle. Un - * fichier corrompu — ou malveillant — peut faire pointer un IFD sur lui-même, - * ce qu'aucun fichier réel ne fait. + * An already visited IFD ends the walk: that is the anti-loop safeguard. A + * corrupted — or malicious — file can make an IFD point to itself, which no + * real file does. * * @return list * - * @throws CorruptedFileException si un offset de la chaîne est hors bornes + * @throws CorruptedFileException if an offset of the chain is out of bounds */ public function readIfdOffsets(): array { @@ -151,11 +151,11 @@ public function readIfdOffsets(): array } /** - * Lit les entrées d'un IFD situé à l'offset donné. + * Reads the entries of an IFD located at the given offset. * * @return list * - * @throws CorruptedFileException si l'offset est hors bornes ou l'IFD tronqué + * @throws CorruptedFileException if the offset is out of bounds or the IFD truncated */ public function readIfd(int $offset): array { @@ -172,9 +172,9 @@ public function readIfd(int $offset): array } /** - * Construit une entrée à partir de ses 12 octets, valeurs résolues comprises. + * Builds an entry from its 12 bytes, resolved values included. * - * @throws CorruptedFileException si un offset indirect est hors bornes + * @throws CorruptedFileException if an indirect offset is out of bounds */ private function readEntry(string $bytes): IfdEntry { @@ -185,36 +185,35 @@ private function readEntry(string $bytes): IfdEntry $size = self::TYPE_SIZES[$type] ?? null; - // Un type inconnu n'est pas une corruption : l'entrée reste lisible, - // seule sa valeur est indéterminable. + // An unknown type is not a corruption: the entry stays readable, only + // its value is undeterminable. if (null === $size || $count < 1) { return new IfdEntry($tag, $type, $count); } $length = $size * $count; - // Une valeur ne peut pas être plus grande que le fichier qui la porte : - // c'est une taille absurde, donc une structure qui ment. + // A value cannot be larger than the file that carries it: that is an + // absurd size, hence a structure that lies. if ($length > $this->fileSize) { throw new CorruptedFileException(sprintf( - 'Entrée 0x%04X : %d octets annoncés, plus que le fichier entier (%d).', + 'Entry 0x%04X: %d bytes announced, more than the whole file (%d).', $tag, $length, $this->fileSize, )); } - // Trop gros pour être un tag que ce package exploite : on garde l'entrée - // — elle reste traversable — mais on ne lit pas sa valeur. Un MakerNote - // de 75 Ko n'est pas une corruption, c'est une métadonnée qui ne nous - // regarde pas. + // Too big to be a tag this package uses: we keep the entry — it stays + // traversable — but we do not read its value. A 75 KB MakerNote is not + // a corruption, it is metadata that is none of our business. if ($length > self::MAX_RESOLVED_VALUE_LENGTH) { return new IfdEntry($tag, $type, $count); } - // Règle des 4 octets : au-delà, le champ porte un offset absolu et non - // la valeur elle-même. S'y tromper donne des offsets qui ressemblent à - // des données valides. + // The 4-byte rule: beyond that, the field carries an absolute offset and + // not the value itself. Getting it wrong yields offsets that look like + // valid data. $raw = $length <= 4 ? substr($field, 0, $length) : $this->readBytes($this->unpackLong($field), $length); @@ -227,19 +226,19 @@ private function readEntry(string $bytes): IfdEntry } /** - * Lit `$length` octets bruts à partir de `$offset`. + * Reads `$length` raw bytes starting from `$offset`. * - * @throws CorruptedFileException si la plage sort du fichier + * @throws CorruptedFileException if the range falls outside the file */ public function readBytes(int $offset, int $length): string { if ($length <= 0) { - throw new CorruptedFileException(sprintf('Longueur de lecture invalide : %d.', $length)); + throw new CorruptedFileException(sprintf('Invalid read length: %d.', $length)); } if ($offset < 0 || $offset + $length > $this->fileSize) { throw new CorruptedFileException(sprintf( - 'Lecture hors bornes : %d octets à l\'offset %d (taille du fichier : %d).', + 'Read out of bounds: %d bytes at offset %d (file size: %d).', $length, $offset, $this->fileSize, @@ -248,17 +247,17 @@ public function readBytes(int $offset, int $length): string fseek($this->handle, $offset); - // La plage est déjà bornée contre fileSize : fread rend exactement - // $length octets. + // The range is already bounded against fileSize: fread returns exactly + // $length bytes. return (string) fread($this->handle, $length); } /** - * Décode une suite d'entiers selon l'endianness du fichier. + * Decodes a sequence of integers according to the file's endianness. * - * Les octets sont toujours fournis par readBytes(), qui garantit déjà leur - * longueur : inutile de la revérifier ici. TYPE_SIZES ne contient que des - * tailles de 1, 2, 4 ou 8 octets. + * The bytes always come from readBytes(), which already guarantees their + * length: no need to check it again here. TYPE_SIZES only contains sizes of + * 1, 2, 4 or 8 bytes. * * @return list */ @@ -272,8 +271,8 @@ private function unpackIntegers(string $bytes, int $size, int $count): array $values[] = match ($size) { 1 => ord($chunk), 2 => $this->unpackShort($chunk), - // RATIONAL et DOUBLE (8 octets) : seuls les 4 premiers nous - // intéressent — aucun tag exploité ici n'a besoin du reste. + // RATIONAL and DOUBLE (8 bytes): only the first 4 are of + // interest to us — no tag used here needs the rest. default => $this->unpackLong(substr($chunk, 0, 4)), }; } @@ -282,7 +281,7 @@ private function unpackIntegers(string $bytes, int $size, int $count): array } /** - * Décode un entier 16 bits selon l'endianness du fichier. + * Decodes a 16-bit integer according to the file's endianness. */ private function unpackShort(string $bytes): int { @@ -290,9 +289,9 @@ private function unpackShort(string $bytes): int } /** - * Décode un entier 32 bits selon l'endianness du fichier. + * Decodes a 32-bit integer according to the file's endianness. * - * @throws CorruptedFileException si les octets fournis ne font pas 4 octets + * @throws CorruptedFileException if the bytes supplied are not 4 bytes long */ private function unpackLong(string $bytes): int { @@ -300,13 +299,13 @@ private function unpackLong(string $bytes): int } /** - * @throws CorruptedFileException si l'offset est invalide ou le compte absurde + * @throws CorruptedFileException if the offset is invalid or the count absurd */ private function readEntryCount(int $offset): int { if ($offset < self::HEADER_LENGTH) { throw new CorruptedFileException(sprintf( - 'Offset d\'IFD invalide : %d chevauche l\'en-tête.', + 'Invalid IFD offset: %d overlaps the header.', $offset, )); } @@ -315,17 +314,17 @@ private function readEntryCount(int $offset): int if ($count > self::MAX_ENTRIES_PER_IFD) { throw new CorruptedFileException(sprintf( - 'Nombre d\'entrées d\'IFD absurde : %d.', + 'Absurd IFD entry count: %d.', $count, )); } - // Le fichier doit contenir toutes les entrées annoncées, plus le lien suivant. + // The file must contain every announced entry, plus the next link. $required = $offset + 2 + $count * self::ENTRY_LENGTH + 4; if ($required > $this->fileSize) { throw new CorruptedFileException(sprintf( - 'IFD tronqué : %d entrées annoncées à l\'offset %d dépassent la taille du fichier.', + 'Truncated IFD: %d entries announced at offset %d exceed the file size.', $count, $offset, )); @@ -335,7 +334,7 @@ private function readEntryCount(int $offset): int } /** - * @throws CorruptedFileException si l'IFD est tronqué + * @throws CorruptedFileException if the IFD is truncated */ private function readNextIfdOffset(int $offset): int { diff --git a/src/Parser/Tiff/TiffTag.php b/src/Parser/Tiff/TiffTag.php index 9cd3232..18505fb 100644 --- a/src/Parser/Tiff/TiffTag.php +++ b/src/Parser/Tiff/TiffTag.php @@ -5,47 +5,47 @@ namespace RonanLenouvel\RawPreviewExtractor\Parser\Tiff; /** - * Tags TIFF/EXIF utiles à la localisation d'une preview JPEG. + * TIFF/EXIF tags useful for locating a JPEG preview. * - * Cette énumération ne prétend pas couvrir TIFF 6.0 : seuls figurent les tags - * que le package exploite réellement. Un tag absent d'ici n'est pas une erreur, - * il est simplement ignoré au parcours. + * This enum does not claim to cover TIFF 6.0: only the tags the package + * actually uses appear here. A tag missing from this list is not an error, it + * is simply ignored while walking. */ enum TiffTag: int { - /** Largeur de l'image décrite par l'IFD courant. */ + /** Width of the image described by the current IFD. */ case ImageWidth = 0x0100; - /** Hauteur de l'image décrite par l'IFD courant. */ + /** Height of the image described by the current IFD. */ case ImageLength = 0x0101; - /** Schéma de compression : 6 ou 7 = JPEG, 1 = non compressé. */ + /** Compression scheme: 6 or 7 = JPEG, 1 = uncompressed. */ case Compression = 0x0103; - /** Fabricant de l'appareil. */ + /** Camera manufacturer. */ case Make = 0x010F; - /** Modèle de l'appareil. */ + /** Camera model. */ case Model = 0x0110; - /** Offset(s) des bandes de données image. */ + /** Offset(s) of the image data strips. */ case StripOffsets = 0x0111; - /** Taille(s) des bandes de données image. */ + /** Size(s) of the image data strips. */ case StripByteCounts = 0x0117; - /** Offsets des sous-IFD — souvent l'emplacement de la preview. */ + /** Offsets of the SubIFDs — often where the preview lives. */ case SubIfds = 0x014A; - /** Offset du JPEG embarqué. */ + /** Offset of the embedded JPEG. */ case JpegInterchangeFormat = 0x0201; - /** Taille du JPEG embarqué, en octets. */ + /** Size of the embedded JPEG, in bytes. */ case JpegInterchangeFormatLength = 0x0202; - /** Pointeur vers l'IFD EXIF. */ + /** Pointer to the EXIF IFD. */ case ExifIfdPointer = 0x8769; - /** Présent uniquement dans les DNG. */ + /** Present only in DNG files. */ case DngVersion = 0xC612; } diff --git a/src/RawPreviewExtractor.php b/src/RawPreviewExtractor.php index f602517..f98ae33 100644 --- a/src/RawPreviewExtractor.php +++ b/src/RawPreviewExtractor.php @@ -13,11 +13,11 @@ use RonanLenouvel\RawPreviewExtractor\Parser\Tiff\TiffPreviewParser; /** - * Façade du package : détecte le format, délègue au parseur qui le prend en charge. + * Package facade: detects the format, delegates to the parser that handles it. * - * Elle ne parse rien elle-même. La résolution se fait par **clé dans une map - * injectée**, jamais par un `switch` sur le format : ajouter RAF ou ORF revient - * à câbler une entrée de plus, sans toucher à cette classe. + * It parses nothing itself. Resolution happens by **key in an injected map**, + * never by a `switch` on the format: adding RAF or ORF amounts to wiring one + * more entry, without touching this class. * * ```php * $extractor = RawPreviewExtractor::createDefault(); @@ -30,12 +30,12 @@ final class RawPreviewExtractor implements RawPreviewExtractorInterface private readonly array $parsers; /** - * @param FormatDetectorInterface $detector identifie le format par signature - * @param iterable $parsers parseurs indexés par - * {@see Format::value} ; un - * `iterable` pour accepter aussi - * bien un tableau que les services - * taggés de Symfony + * @param FormatDetectorInterface $detector identifies the format by signature + * @param iterable $parsers parsers indexed by + * {@see Format::value}; an + * `iterable` so as to accept + * either an array or Symfony's + * tagged services */ public function __construct( private readonly FormatDetectorInterface $detector, @@ -45,11 +45,11 @@ public function __construct( } /** - * Construit un extracteur câblé avec les parseurs standards. + * Builds an extractor wired with the standard parsers. * - * Raccourci pour les projets sans conteneur d'injection : une ligne au lieu - * de l'assemblage manuel. Sous Symfony, le bundle câble les mêmes services - * et c'est lui qui fait foi. + * A shortcut for projects without a dependency injection container: one line + * instead of manual assembly. Under Symfony, the bundle wires the same + * services and it is the bundle that is authoritative. */ public static function createDefault(): self { @@ -71,7 +71,7 @@ public function extract(string $path): ExtractedPreview if (null === $format || null === $parser) { throw new UnsupportedFormatException(sprintf( - 'Format non supporté : %s n\'est pas un RAW que ce package sait lire.', + 'Unsupported format: %s is not a RAW file this package can read.', basename($path), )); } @@ -83,16 +83,16 @@ public function supports(string $path): bool { $format = $this->detector->detect($path); - // Le détecteur peut reconnaître un format qu'aucun parseur ne traite : - // supports() doit refléter ce que la façade sait vraiment faire. + // The detector may recognise a format that no parser handles: + // supports() must reflect what the facade can really do. return null !== $format && isset($this->parsers[$format->value]); } /** - * Un parseur est-il câblé pour ce format ? + * Is a parser wired for this format? * - * Sert aux tests de câblage — de la fabrique comme du bundle — pour vérifier - * qu'aucun cas de {@see Format} n'a été oublié. + * Used by the wiring tests — of the factory as well as the bundle — to check + * that no {@see Format} case has been forgotten. */ public function hasParserFor(Format $format): bool { diff --git a/src/RawPreviewExtractorInterface.php b/src/RawPreviewExtractorInterface.php index 0145335..f617ebf 100644 --- a/src/RawPreviewExtractorInterface.php +++ b/src/RawPreviewExtractorInterface.php @@ -10,9 +10,9 @@ use RonanLenouvel\RawPreviewExtractor\Exception\UnsupportedFormatException; /** - * Extrait la preview JPEG embarquée dans un fichier RAW. + * Extracts the JPEG preview embedded in a RAW file. * - * C'est le point d'entrée du package — le seul type à connaître pour l'utiliser : + * This is the package's entry point — the only type to know in order to use it: * * ```php * $extractor = RawPreviewExtractor::createDefault(); @@ -21,36 +21,36 @@ * $preview = $extractor->extract('/photos/IMG_0042.CR2'); * file_put_contents('/cache/thumb.jpg', $preview->jpegData); * } catch (RawPreviewExtractorException) { - * // pas de vignette : l'appelant dégrade comme il l'entend + * // no thumbnail: the caller degrades as it sees fit * } * ``` * - * Sous Symfony, le bundle enregistre une implémentation auto-wirable de cette - * interface : c'est elle qu'on type-hinte, jamais la classe concrète. + * Under Symfony, the bundle registers an autowirable implementation of this + * interface: it is the one to type-hint, never the concrete class. */ interface RawPreviewExtractorInterface { /** - * Extrait la preview JPEG du fichier RAW. + * Extracts the JPEG preview from the RAW file. * - * Toutes les exceptions levées implémentent {@see RawPreviewExtractorException} : - * un seul `catch` suffit pour dégrader proprement. + * Every exception thrown implements {@see RawPreviewExtractorException}: a + * single `catch` is enough to degrade gracefully. * - * @param string $path chemin du fichier RAW + * @param string $path path of the RAW file * - * @throws UnsupportedFormatException le fichier n'est pas un RAW pris en charge - * @throws PreviewNotFoundException fichier valide, mais sans preview JPEG - * @throws CorruptedFileException fichier illisible ou structurellement invalide + * @throws UnsupportedFormatException the file is not a supported RAW + * @throws PreviewNotFoundException valid file, but without a JPEG preview + * @throws CorruptedFileException unreadable or structurally invalid file */ public function extract(string $path): ExtractedPreview; /** - * Ce fichier peut-il être traité par ce package ? + * Can this file be processed by this package? * - * La réponse repose sur la **signature binaire** du fichier, jamais sur son - * extension : un CR2 renommé en `.jpg` renvoie `true`. + * The answer rests on the file's **binary signature**, never on its + * extension: a CR2 renamed to `.jpg` returns `true`. * - * Ne lève jamais — un fichier absent ou illisible renvoie simplement `false`. + * Never throws — a missing or unreadable file simply returns `false`. */ public function supports(string $path): bool; } diff --git a/tests/Unit/Parser/Cr3/IsoBmffBoxReaderTest.php b/tests/Unit/Parser/Cr3/IsoBmffBoxReaderTest.php index 605c01b..7883cdb 100644 --- a/tests/Unit/Parser/Cr3/IsoBmffBoxReaderTest.php +++ b/tests/Unit/Parser/Cr3/IsoBmffBoxReaderTest.php @@ -109,7 +109,7 @@ public function testHandlesSize64Bit(): void public function testThrowsOnSizeSmallerThanHeader(): void { $this->expectException(CorruptedFileException::class); - $this->expectExceptionMessage('Taille de boîte'); + $this->expectExceptionMessage('Invalid box size'); // size = 4 : plus petit que les 8 octets d'en-tête. Sans garde, l'offset // reculerait et la lecture partirait en boucle. @@ -129,7 +129,7 @@ public function testThrowsOnSize64BitSmallerThanHeader(): void public function testThrowsOnSizeBeyondFile(): void { $this->expectException(CorruptedFileException::class); - $this->expectExceptionMessage('hors bornes'); + $this->expectExceptionMessage('out of bounds'); // La boîte annonce 9999 octets dans un fichier qui en fait 16. $this->reader(pack('N', 9999) . 'mdat' . str_repeat("\x00", 8))->readBoxes(); @@ -146,7 +146,7 @@ public function testThrowsOnTruncatedFile(): void public function testThrowsOnMissingFile(): void { $this->expectException(CorruptedFileException::class); - $this->expectExceptionMessage('illisible'); + $this->expectExceptionMessage('Unreadable file'); new IsoBmffBoxReader('/chemin/inexistant.cr3'); } @@ -193,7 +193,7 @@ public function testReadsPayload(): void public function testThrowsWhenReadingBytesOutOfBounds(): void { $this->expectException(CorruptedFileException::class); - $this->expectExceptionMessage('hors bornes'); + $this->expectExceptionMessage('out of bounds'); // readBytes est l'API publique du reader : Cr3PreviewParser l'appellera // avec des offsets issus du fichier, donc non fiables. diff --git a/tests/Unit/Parser/Tiff/TiffReaderTest.php b/tests/Unit/Parser/Tiff/TiffReaderTest.php index 4788d52..1b6b2b7 100644 --- a/tests/Unit/Parser/Tiff/TiffReaderTest.php +++ b/tests/Unit/Parser/Tiff/TiffReaderTest.php @@ -44,7 +44,7 @@ public function testReadsBigEndianHeader(): void public function testThrowsOnUnknownByteOrder(): void { $this->expectException(CorruptedFileException::class); - $this->expectExceptionMessage('Ordre d\'octets'); + $this->expectExceptionMessage('Unknown byte order'); $this->reader("XX\x2a\x00" . pack('V', 8) . str_repeat("\x00", 8)); } @@ -52,7 +52,7 @@ public function testThrowsOnUnknownByteOrder(): void public function testThrowsOnWrongMagicNumber(): void { $this->expectException(CorruptedFileException::class); - $this->expectExceptionMessage('Nombre magique'); + $this->expectExceptionMessage('Invalid TIFF magic'); // 43 au lieu de 42 : l'en-tête a la bonne forme mais n'est pas du TIFF. $this->reader('II' . pack('v', 43) . pack('V', 8) . str_repeat("\x00", 8)); @@ -69,7 +69,7 @@ public function testThrowsOnTruncatedFile(): void public function testThrowsOnMissingFile(): void { $this->expectException(CorruptedFileException::class); - $this->expectExceptionMessage('illisible'); + $this->expectExceptionMessage('Unreadable file'); new TiffReader('/chemin/inexistant.cr2'); } @@ -255,7 +255,7 @@ public function testStillResolvesLargeValuesOfExploitedTags(): void public function testThrowsWhenIfdOffsetIsOutOfBounds(): void { $this->expectException(CorruptedFileException::class); - $this->expectExceptionMessage('hors bornes'); + $this->expectExceptionMessage('out of bounds'); $this->reader('II' . pack('v', 42) . pack('V', 9999) . str_repeat("\x00", 8)) ->readIfd(9999); @@ -273,7 +273,7 @@ public function testThrowsWhenIfdOverlapsHeader(): void public function testThrowsWhenIfdIsTruncatedMidEntry(): void { $this->expectException(CorruptedFileException::class); - $this->expectExceptionMessage('tronqué'); + $this->expectExceptionMessage('Truncated'); // Annonce 3 entrées, le fichier s'arrête au milieu de la première. $this->reader('II' . pack('v', 42) . pack('V', 8) . pack('v', 3) . "\x0F\x01\x02") diff --git a/tests/Unit/RawPreviewExtractorTest.php b/tests/Unit/RawPreviewExtractorTest.php index 09c6f15..39e05d0 100644 --- a/tests/Unit/RawPreviewExtractorTest.php +++ b/tests/Unit/RawPreviewExtractorTest.php @@ -56,7 +56,7 @@ public function testExtractPassesDetectedFormatToTheParser(): void public function testThrowsUnsupportedFormatForNonRaw(): void { $this->expectException(UnsupportedFormatException::class); - $this->expectExceptionMessage('non supporté'); + $this->expectExceptionMessage('Unsupported format'); (new RawPreviewExtractor($this->detectorReturning(null), []))->extract('/photo.jpg'); }