├── .credo.exs ├── .formatter.exs ├── .github └── workflows │ ├── build.yml │ ├── build_elixir_latest.yml │ ├── build_elixir_old.yml │ ├── build_elixir_recent.yml │ ├── build_reusable.yml │ └── ci.yml ├── .gitignore ├── .tool-versions ├── CHANGELOG.md ├── CONTRIBUTORS.md ├── LICENSE.md ├── README.md ├── VERSION ├── assets └── ex_image_info_benchmarks.png ├── lib ├── ex_image_info.ex └── ex_image_info │ ├── detector.ex │ ├── parsers │ └── isobmff.ex │ └── types │ ├── avif.ex │ ├── bmp.ex │ ├── gif.ex │ ├── heic.ex │ ├── heif.ex │ ├── ico.ex │ ├── jp2.ex │ ├── jpeg.ex │ ├── png.ex │ ├── pnm.ex │ ├── psd.ex │ ├── tiff.ex │ └── webp.ex ├── mix.exs ├── mix.lock └── test ├── ex_image_info_test.exs ├── ex_image_info_test ├── images │ ├── bmp_test.exs │ ├── gif_test.exs │ ├── ico_test.exs │ ├── isobmff_test.exs │ ├── jp2_test.exs │ ├── jpeg_test.exs │ ├── png_test.exs │ ├── pnm_test.exs │ ├── psd_test.exs │ ├── tiff_test.exs │ └── webp_test.exs └── mocks │ ├── bmp_test.exs │ ├── gif_test.exs │ ├── ico_test.exs │ ├── isobmff_test.exs │ ├── jp2_test.exs │ ├── jpeg_test.exs │ ├── png_test.exs │ ├── pnm_test.exs │ ├── psd_test.exs │ ├── tiff_test.exs │ └── webp_test.exs ├── fixtures ├── images │ └── valid │ │ ├── bmp │ │ └── layers.bmp │ │ ├── gif │ │ ├── layers-87a.gif │ │ ├── layers-anim.gif │ │ └── layers.gif │ │ ├── ico │ │ ├── w.ico │ │ └── wikipedia.ico │ │ ├── isobmff │ │ ├── layers-min.heic │ │ ├── layers-rotated.heic │ │ ├── layers.avif │ │ ├── layers.heic │ │ └── layers.heif │ │ ├── jp2 │ │ └── layers.jp2 │ │ ├── jpeg │ │ ├── layers-progressive.jpeg │ │ └── layers.jpeg │ │ ├── png │ │ └── layers.png │ │ ├── pnm │ │ ├── layers.pbm │ │ ├── layers.pgm │ │ ├── layers.ppm │ │ ├── plain.pbm │ │ ├── plain.pgm │ │ └── plain.ppm │ │ ├── psd │ │ └── layers.psd │ │ ├── tiff │ │ ├── i-s-big-endian.tiff │ │ ├── layers-deflate.tiff │ │ ├── layers-jpeg.tiff │ │ ├── layers-lzw.tiff │ │ ├── layers-packbits.tiff │ │ └── layers.tiff │ │ └── webp │ │ ├── layers-anim.webp │ │ ├── layers-lossless.webp │ │ └── layers-lossy.webp └── mocks │ └── isobmff.exs ├── support └── image_test_case.ex └── test_helper.exs /.credo.exs: -------------------------------------------------------------------------------- 1 | # This file contains the configuration for Credo and you are probably reading 2 | # this after creating it with `mix credo.gen.config`. 3 | # 4 | # If you find anything wrong or unclear in this file, please report an 5 | # issue on GitHub: https://github.com/rrrene/credo/issues 6 | # 7 | %{ 8 | # 9 | # You can have as many configs as you like in the `configs:` field. 10 | configs: [ 11 | %{ 12 | # 13 | # Run any config using `mix credo -C `. If no config name is given 14 | # "default" is used. 15 | # 16 | name: "default", 17 | # 18 | # These are the files included in the analysis: 19 | files: %{ 20 | # 21 | # You can give explicit globs or simply directories. 22 | # In the latter case `**/*.{ex,exs}` will be used. 23 | # 24 | included: [ 25 | "lib/", 26 | "src/", 27 | "test/", 28 | "web/", 29 | "apps/*/lib/", 30 | "apps/*/src/", 31 | "apps/*/test/", 32 | "apps/*/web/" 33 | ], 34 | excluded: [~r"/_build/", ~r"/deps/", ~r"/node_modules/"] 35 | }, 36 | # 37 | # Load and configure plugins here: 38 | # 39 | plugins: [], 40 | # 41 | # If you create your own checks, you must specify the source files for 42 | # them here, so they can be loaded by Credo before running the analysis. 43 | # 44 | requires: [], 45 | # 46 | # If you want to enforce a style guide and need a more traditional linting 47 | # experience, you can change `strict` to `true` below: 48 | # 49 | strict: false, 50 | # 51 | # To modify the timeout for parsing files, change this value: 52 | # 53 | parse_timeout: 5000, 54 | # 55 | # If you want to use uncolored output by default, you can change `color` 56 | # to `false` below: 57 | # 58 | color: true, 59 | # 60 | # You can customize the parameters of any check by adding a second element 61 | # to the tuple. 62 | # 63 | # To disable a check put `false` as second element: 64 | # 65 | # {Credo.Check.Design.DuplicatedCode, false} 66 | # 67 | checks: %{ 68 | enabled: [ 69 | # 70 | ## Consistency Checks 71 | # 72 | {Credo.Check.Consistency.ExceptionNames, []}, 73 | {Credo.Check.Consistency.LineEndings, []}, 74 | {Credo.Check.Consistency.ParameterPatternMatching, []}, 75 | {Credo.Check.Consistency.SpaceAroundOperators, []}, 76 | {Credo.Check.Consistency.SpaceInParentheses, []}, 77 | {Credo.Check.Consistency.TabsOrSpaces, []}, 78 | 79 | # 80 | ## Design Checks 81 | # 82 | # You can customize the priority of any check 83 | # Priority values are: `low, normal, high, higher` 84 | # 85 | {Credo.Check.Design.AliasUsage, 86 | [priority: :low, if_nested_deeper_than: 2, if_called_more_often_than: 0]}, 87 | {Credo.Check.Design.TagFIXME, []}, 88 | # You can also customize the exit_status of each check. 89 | # If you don't want TODO comments to cause `mix credo` to fail, just 90 | # set this value to 0 (zero). 91 | # 92 | {Credo.Check.Design.TagTODO, [exit_status: 2]}, 93 | 94 | # 95 | ## Readability Checks 96 | # 97 | {Credo.Check.Readability.AliasOrder, []}, 98 | {Credo.Check.Readability.FunctionNames, []}, 99 | {Credo.Check.Readability.LargeNumbers, []}, 100 | {Credo.Check.Readability.MaxLineLength, [priority: :low, max_length: 120]}, 101 | {Credo.Check.Readability.ModuleAttributeNames, []}, 102 | {Credo.Check.Readability.ModuleDoc, []}, 103 | {Credo.Check.Readability.ModuleNames, []}, 104 | {Credo.Check.Readability.ParenthesesInCondition, []}, 105 | {Credo.Check.Readability.ParenthesesOnZeroArityDefs, []}, 106 | {Credo.Check.Readability.PipeIntoAnonymousFunctions, []}, 107 | {Credo.Check.Readability.PredicateFunctionNames, []}, 108 | {Credo.Check.Readability.PreferImplicitTry, []}, 109 | {Credo.Check.Readability.RedundantBlankLines, []}, 110 | {Credo.Check.Readability.Semicolons, []}, 111 | {Credo.Check.Readability.SpaceAfterCommas, []}, 112 | {Credo.Check.Readability.StringSigils, []}, 113 | {Credo.Check.Readability.TrailingBlankLine, []}, 114 | {Credo.Check.Readability.TrailingWhiteSpace, []}, 115 | {Credo.Check.Readability.UnnecessaryAliasExpansion, []}, 116 | {Credo.Check.Readability.VariableNames, []}, 117 | {Credo.Check.Readability.WithSingleClause, []}, 118 | 119 | # 120 | ## Refactoring Opportunities 121 | # 122 | {Credo.Check.Refactor.Apply, []}, 123 | {Credo.Check.Refactor.CondStatements, []}, 124 | {Credo.Check.Refactor.CyclomaticComplexity, []}, 125 | {Credo.Check.Refactor.FilterCount, []}, 126 | {Credo.Check.Refactor.FilterFilter, []}, 127 | {Credo.Check.Refactor.FunctionArity, [ignore_defp: true]}, 128 | {Credo.Check.Refactor.LongQuoteBlocks, []}, 129 | {Credo.Check.Refactor.MapJoin, []}, 130 | {Credo.Check.Refactor.MatchInCondition, []}, 131 | {Credo.Check.Refactor.NegatedConditionsInUnless, []}, 132 | {Credo.Check.Refactor.NegatedConditionsWithElse, []}, 133 | {Credo.Check.Refactor.Nesting, []}, 134 | {Credo.Check.Refactor.RedundantWithClauseResult, []}, 135 | {Credo.Check.Refactor.RejectReject, []}, 136 | {Credo.Check.Refactor.UnlessWithElse, []}, 137 | {Credo.Check.Refactor.WithClauses, []}, 138 | 139 | # 140 | ## Warnings 141 | # 142 | {Credo.Check.Warning.ApplicationConfigInModuleAttribute, []}, 143 | {Credo.Check.Warning.BoolOperationOnSameValues, []}, 144 | {Credo.Check.Warning.Dbg, []}, 145 | {Credo.Check.Warning.ExpensiveEmptyEnumCheck, []}, 146 | {Credo.Check.Warning.IExPry, []}, 147 | {Credo.Check.Warning.IoInspect, []}, 148 | {Credo.Check.Warning.MissedMetadataKeyInLoggerConfig, []}, 149 | {Credo.Check.Warning.OperationOnSameValues, []}, 150 | {Credo.Check.Warning.OperationWithConstantResult, []}, 151 | {Credo.Check.Warning.RaiseInsideRescue, []}, 152 | {Credo.Check.Warning.SpecWithStruct, []}, 153 | {Credo.Check.Warning.UnsafeExec, []}, 154 | {Credo.Check.Warning.UnusedEnumOperation, []}, 155 | {Credo.Check.Warning.UnusedFileOperation, []}, 156 | {Credo.Check.Warning.UnusedKeywordOperation, []}, 157 | {Credo.Check.Warning.UnusedListOperation, []}, 158 | {Credo.Check.Warning.UnusedPathOperation, []}, 159 | {Credo.Check.Warning.UnusedRegexOperation, []}, 160 | {Credo.Check.Warning.UnusedStringOperation, []}, 161 | {Credo.Check.Warning.UnusedTupleOperation, []}, 162 | {Credo.Check.Warning.WrongTestFileExtension, []} 163 | ], 164 | disabled: [ 165 | # 166 | # Checks scheduled for next check update (opt-in for now, just replace `false` with `[]`) 167 | 168 | # 169 | # Controversial and experimental checks (opt-in, just move the check to `:enabled` 170 | # and be sure to use `mix credo --strict` to see low priority checks) 171 | # 172 | {Credo.Check.Consistency.MultiAliasImportRequireUse, []}, 173 | {Credo.Check.Consistency.UnusedVariableNames, []}, 174 | {Credo.Check.Design.DuplicatedCode, []}, 175 | {Credo.Check.Design.SkipTestWithoutComment, []}, 176 | {Credo.Check.Readability.AliasAs, []}, 177 | {Credo.Check.Readability.BlockPipe, []}, 178 | {Credo.Check.Readability.ImplTrue, []}, 179 | {Credo.Check.Readability.MultiAlias, []}, 180 | {Credo.Check.Readability.NestedFunctionCalls, []}, 181 | {Credo.Check.Readability.OneArityFunctionInPipe, []}, 182 | {Credo.Check.Readability.OnePipePerLine, []}, 183 | {Credo.Check.Readability.SeparateAliasRequire, []}, 184 | {Credo.Check.Readability.SingleFunctionToBlockPipe, []}, 185 | {Credo.Check.Readability.SinglePipe, []}, 186 | {Credo.Check.Readability.Specs, []}, 187 | {Credo.Check.Readability.StrictModuleLayout, []}, 188 | {Credo.Check.Readability.WithCustomTaggedTuple, []}, 189 | {Credo.Check.Refactor.ABCSize, []}, 190 | {Credo.Check.Refactor.AppendSingleItem, []}, 191 | {Credo.Check.Refactor.DoubleBooleanNegation, []}, 192 | {Credo.Check.Refactor.FilterReject, []}, 193 | {Credo.Check.Refactor.IoPuts, []}, 194 | {Credo.Check.Refactor.MapMap, []}, 195 | {Credo.Check.Refactor.ModuleDependencies, []}, 196 | {Credo.Check.Refactor.NegatedIsNil, []}, 197 | {Credo.Check.Refactor.PassAsyncInTestCases, []}, 198 | {Credo.Check.Refactor.PipeChainStart, []}, 199 | {Credo.Check.Refactor.RejectFilter, []}, 200 | {Credo.Check.Refactor.VariableRebinding, []}, 201 | {Credo.Check.Warning.LazyLogging, []}, 202 | {Credo.Check.Warning.LeakyEnvironment, []}, 203 | {Credo.Check.Warning.MapGetUnsafePass, []}, 204 | {Credo.Check.Warning.MixEnv, []}, 205 | {Credo.Check.Warning.UnsafeToAtom, []} 206 | 207 | # {Credo.Check.Refactor.MapInto, []}, 208 | 209 | # 210 | # Custom checks can be created using `mix credo.gen.check`. 211 | # 212 | ] 213 | } 214 | } 215 | ] 216 | } 217 | -------------------------------------------------------------------------------- /.formatter.exs: -------------------------------------------------------------------------------- 1 | # Used by "mix format" 2 | [ 3 | plugins: [Styler], 4 | inputs: [ 5 | "{mix,.formatter}.exs", 6 | "{lib,bench}/**/*.{ex,exs}" 7 | | ["test/**/*.{ex,exs}"] 8 | |> Enum.flat_map(&Path.wildcard(&1, match_dot: true)) 9 | |> Kernel.--([ 10 | "test/fixtures/mocks/isobmff.exs" 11 | ]) 12 | ], 13 | line_length: 88 14 | ] 15 | -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: Build CI 2 | 3 | on: 4 | push: 5 | branches: ["*"] 6 | pull_request: 7 | branches: ["*"] 8 | 9 | jobs: 10 | build: 11 | runs-on: ${{ matrix.os }} 12 | name: Build as in prod 13 | env: 14 | MIX_ENV: prod 15 | strategy: 16 | matrix: 17 | os: ["ubuntu-22.04"] 18 | elixir: ["1.18", "1.16", "1.14.5"] 19 | otp: ["27", "26", "25"] 20 | exclude: 21 | - elixir: "1.18" 22 | otp: "25" 23 | - elixir: "1.16" 24 | otp: "27" 25 | - elixir: "1.14.5" 26 | otp: "27" 27 | - elixir: "1.14.5" 28 | otp: "26" 29 | steps: 30 | - uses: actions/checkout@v3 31 | - uses: erlef/setup-beam@v1 32 | with: 33 | otp-version: ${{ matrix.otp }} 34 | elixir-version: ${{ matrix.elixir }} 35 | - uses: actions/cache@v3 36 | with: 37 | path: deps 38 | key: ${{ matrix.os }}-otp_${{ matrix.otp }}-elixir_${{ matrix.elixir }}-mix_${{ hashFiles('**/mix.lock') }} 39 | restore-keys: ${{ matrix.os }}-otp_${{ matrix.otp }}-elixir_${{ matrix.elixir }}-mix_ 40 | - run: mix deps.get 41 | - run: mix compile --warnings-as-errors 42 | -------------------------------------------------------------------------------- /.github/workflows/build_elixir_latest.yml: -------------------------------------------------------------------------------- 1 | # ' ' suffix for the badge status 2 | name: "Build 1.18/27 " 3 | 4 | on: 5 | push: 6 | branches: 7 | - '*' 8 | pull_request: 9 | branches: 10 | - '*' 11 | 12 | jobs: 13 | elixir_otp_latest: 14 | uses: ./.github/workflows/build_reusable.yml 15 | with: 16 | elixir-version: 1.18 17 | otp-version: 27 18 | -------------------------------------------------------------------------------- /.github/workflows/build_elixir_old.yml: -------------------------------------------------------------------------------- 1 | # ' ' suffix for the badge status 2 | name: "Build 1.14/25 " 3 | 4 | on: 5 | push: 6 | branches: 7 | - '*' 8 | pull_request: 9 | branches: 10 | - '*' 11 | 12 | jobs: 13 | elixir_otp_latest: 14 | uses: ./.github/workflows/build_reusable.yml 15 | with: 16 | elixir-version: 1.14.5 17 | otp-version: 25 18 | -------------------------------------------------------------------------------- /.github/workflows/build_elixir_recent.yml: -------------------------------------------------------------------------------- 1 | # ' ' suffix for the badge status 2 | name: "Build 1.16/26 " 3 | 4 | on: 5 | push: 6 | branches: 7 | - '*' 8 | pull_request: 9 | branches: 10 | - '*' 11 | 12 | jobs: 13 | elixir_otp_latest: 14 | uses: ./.github/workflows/build_reusable.yml 15 | with: 16 | elixir-version: 1.16 17 | otp-version: 26 18 | -------------------------------------------------------------------------------- /.github/workflows/build_reusable.yml: -------------------------------------------------------------------------------- 1 | name: Build CI Reusable 2 | 3 | on: 4 | workflow_call: 5 | inputs: 6 | elixir-version: 7 | required: true 8 | type: string 9 | otp-version: 10 | required: true 11 | type: string 12 | 13 | jobs: 14 | build: 15 | runs-on: ubuntu-latest 16 | steps: 17 | - name: Checkout code 18 | uses: actions/checkout@v3 19 | 20 | - name: Setup Elixir 21 | uses: erlef/setup-beam@v1 22 | with: 23 | elixir-version: ${{ inputs.elixir-version }} 24 | otp-version: ${{ inputs.otp-version }} 25 | 26 | - name: Install dependencies 27 | run: mix deps.get 28 | 29 | - name: Run compilation with warnings as errors 30 | run: mix compile --warnings-as-errors 31 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | branches: ["*"] 6 | pull_request: 7 | branches: ["*"] 8 | 9 | jobs: 10 | lint: 11 | runs-on: ${{ matrix.os }} 12 | env: 13 | MIX_ENV: dev 14 | name: Lint 15 | strategy: 16 | matrix: 17 | os: ["ubuntu-22.04"] 18 | elixir: ["1.16", "1.18"] 19 | otp: ["26"] 20 | steps: 21 | - uses: actions/checkout@v3 22 | - uses: erlef/setup-beam@v1 23 | with: 24 | otp-version: ${{ matrix.otp }} 25 | elixir-version: ${{ matrix.elixir }} 26 | - uses: actions/cache@v3 27 | with: 28 | path: deps 29 | key: ${{ matrix.os }}-otp_${{ matrix.otp }}-elixir_${{ matrix.elixir }}-mix_${{ hashFiles('**/mix.lock') }} 30 | restore-keys: ${{ matrix.os }}-otp_${{ matrix.otp }}-elixir_${{ matrix.elixir }}-mix_ 31 | - run: mix deps.get 32 | - run: mix deps.compile 33 | - run: mix lint 34 | 35 | test: 36 | runs-on: ${{ matrix.os }} 37 | name: Test Elixir ${{ matrix.elixir }}, OTP ${{ matrix.otp }}, OS ${{ matrix.os }} 38 | env: 39 | MIX_ENV: test 40 | strategy: 41 | fail-fast: false 42 | matrix: 43 | os: ["ubuntu-22.04"] 44 | elixir: ["1.18", "1.16", "1.14.5"] 45 | otp: ["27", "26", "25"] 46 | exclude: 47 | - elixir: "1.18" 48 | otp: "25" 49 | - elixir: "1.16" 50 | otp: "27" 51 | - elixir: "1.14.5" 52 | otp: "27" 53 | - elixir: "1.14.5" 54 | otp: "26" 55 | steps: 56 | - uses: actions/checkout@v3 57 | - uses: erlef/setup-beam@v1 58 | with: 59 | otp-version: ${{ matrix.otp }} 60 | elixir-version: ${{ matrix.elixir }} 61 | - uses: actions/cache@v3 62 | with: 63 | path: deps 64 | key: ${{ matrix.os }}-otp_${{ matrix.otp }}-elixir_${{ matrix.elixir }}-mix_${{ hashFiles('**/mix.lock') }} 65 | restore-keys: ${{ matrix.os }}-otp_${{ matrix.otp }}-elixir_${{ matrix.elixir }}-mix_ 66 | - run: mix deps.get --only test 67 | - run: mix deps.compile 68 | - run: mix compile --warnings-as-errors 69 | - run: mix test 70 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # -*- mode: conf; -*- 2 | 3 | # Created by https://www.gitignore.io/api/elixir 4 | 5 | ### Elixir ### 6 | /_build 7 | /cover 8 | /deps 9 | /doc 10 | /.fetch 11 | erl_crash.dump 12 | *.ez 13 | *.beam 14 | 15 | ### Elixir Patch ### 16 | # End of https://www.gitignore.io/api/elixir 17 | 18 | # Created by https://www.gitignore.io/api/emacs 19 | 20 | ### Emacs ### 21 | # -*- mode: gitignore; -*- 22 | *~ 23 | \#*\# 24 | /.emacs.desktop 25 | /.emacs.desktop.lock 26 | *.elc 27 | auto-save-list 28 | tramp 29 | .\#* 30 | 31 | # Org-mode 32 | .org-id-locations 33 | *_archive 34 | 35 | # flymake-mode 36 | *_flymake.* 37 | 38 | # eshell files 39 | /eshell/history 40 | /eshell/lastdir 41 | 42 | # elpa packages 43 | /elpa/ 44 | 45 | # reftex files 46 | *.rel 47 | 48 | # AUCTeX auto folder 49 | /auto/ 50 | 51 | # cask packages 52 | .cask/ 53 | dist/ 54 | 55 | # Flycheck 56 | flycheck_*.el 57 | 58 | # server auth directory 59 | /server/ 60 | 61 | # projectiles files 62 | .projectile 63 | projectile-bookmarks.eld 64 | 65 | # directory configuration 66 | .dir-locals.el 67 | 68 | # saveplace 69 | places 70 | 71 | # url cache 72 | url/cache/ 73 | 74 | # cedet 75 | ede-projects.el 76 | 77 | # smex 78 | smex-items 79 | 80 | # company-statistics 81 | company-statistics-cache.el 82 | 83 | # anaconda-mode 84 | anaconda-mode/ 85 | 86 | # End of https://www.gitignore.io/api/emacs 87 | 88 | ## Repo custom rules 89 | 90 | /support 91 | # /docs 92 | 93 | # for gh-pages 94 | !dist/ 95 | /docs/all.json 96 | /docs/ 97 | /.idea/ 98 | 99 | .DS_Store 100 | /tmp/ 101 | -------------------------------------------------------------------------------- /.tool-versions: -------------------------------------------------------------------------------- 1 | elixir 1.18.2-otp-27 2 | erlang 27.2.3 -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # CHANGELOG 2 | 3 | ## v1.0.0 (2025-03-28) 4 | 5 | **Enhancements:** 6 | 7 | - Full support for ISOBMFF image formats (box-type): HEIF, HEIC and AVIF. 8 | - Reordering types being checked in guessing functions (`{seems?,type,info}/1`), focusing on recent trends. 9 | - Documentation and testing improvements. 10 | 11 | **API status and breaking changes:** 12 | 13 | - `seems?/1`: returns the image type found (e.g., `:bmp`, `:tiff`), or `nil` if it is not recognized. 14 | For JPEG images, it returns `:jpeg` instead of the alias `:jpg`. However, the alias `:jpg` can be used in `{seems?,type,info}/2` functions. 15 | - `seems?/2`, `type/2`, and `info/2`: these functions will **raise an error if an unsupported image type is provided**. 16 | - `seems?/2`: **fully boolean**. 17 | 18 | **News:** 19 | 20 | - First major release of ExImageInfo, after years working without issues and really stable. 21 | 22 | **Statistics:** 23 | 24 | - 79 TDD Tests: 25 | - including 32 images being tested, also fetching external images on-the-fly. 26 | - supporting partial streams and including many binary mocks/fixtures (up to 50 tests to verify edge cases). 27 | - Code coverage of 98.3%. 28 | - Supports 13 image formats and 23 variants. 29 | - No dependencies. 30 | 31 | **Acknowledgments:** 32 | 33 | - @bu6n for the first attempts about heic/heif/avif support and starting as a reviewer. 34 | 35 | ## v0.2.7 (2025-03-05) 36 | 37 | **Enhancements:** 38 | 39 | - Support for Elixir 1.18 (removing warnings). 40 | 41 | **News:** 42 | 43 | - Removed GitHub pages (/docs/) as hexdocs.pm is the official documentation site. 44 | - Bumping versions to all dev/test dependencies. 45 | - Improving CI pipelines and fixing HTML-generated docs (badges). 46 | - Removing unneeded docs tasks. 47 | 48 | **Acknowledgments:** 49 | 50 | - Andrew Bruce (@camelpunch) for notifying about 1.18 warnings and starting as a reviewer. 51 | 52 | ## v0.2.6 (2024-01-18) 53 | 54 | **Enhancements:** 55 | 56 | - Improving a parsing case for the PNM format. If its signature is not fully formed, it skips parsing the size. 57 | 58 | **News:** 59 | 60 | - Adding `styler` to format and solve credo styling issues automatically. 61 | 62 | ## v0.2.5 (2024-06-09) 63 | 64 | **News:** 65 | 66 | - Added CI pipelines (test, lint, build). 67 | - Formatting code with `mix format`. 68 | - Bumping versions to all dev/test dependencies. 69 | 70 | **Types:** 71 | 72 | - Rest case for `seems?/2` returns a boolean (`false`) instead of `nil`. 73 | 74 | **Acknowledgments:** 75 | 76 | - Matthew Johnston (@warmwaffles) for all these changes and starting as a reviewer/contributor. 77 | 78 | ## v0.2.4 (2018-11-24) 79 | 80 | **Enhancements:** 81 | 82 | - By request of a GitHub user: support for another variant of webp. 83 | - Studied and integrated the webpVP8X format (bitstream animated). 84 | - Added 2 new tests for animated photos: gif and webp vp8x. 85 | 86 | ## v0.2.3 (2018-05-21) 87 | 88 | **Enhancements:** 89 | 90 | - By request of a GitHub user: added the type *jpg* as an alias of *jpeg*. 91 | 92 | ## v0.2.2 (2017-11-04) 93 | 94 | **News:** 95 | 96 | - Docs are in the official repo, not in gh-pages branch. 97 | - Added inch-ci and ebertapp static analysis online tools (0 issues). 98 | - Repository promoted to the Group4Layers organization. 99 | - New "patch" version to include the changes. 100 | 101 | ## v0.2.1 (2017-11-03) 102 | 103 | **Enhancements:** 104 | 105 | - Code is improved following credo, solving: 106 | - 5 software design suggestions. 107 | - 34 code readability issues. 108 | - 2 refactoring opportunities. 109 | - 2 consistency issues. 110 | - Clean code: removed superfluous comments and refactored def to defp when applicable. 111 | 112 | **News:** 113 | 114 | - Benchmarks are performed. An image with charts is included to compare famous elixir libraries. ExImageInfo always outperforms. 115 | - Online tools applied (TravisCI and Coveralls). Badges included. 116 | - Added ebertapp static analysis online tool. 117 | - Repository promoted to Group4Layers organization. 118 | - New "patch" version to include the changes. 119 | 120 | ## v0.2.0 (2017-06-17) 121 | 122 | **Warnings:** 123 | 124 | - Use with caution the formats *ico*, *jp2* and the family *pnm*. They are implemented without following other libraries (just reading the specs - sometimes working with old drafts like *jp2*). You can support this library by providing more tests and image *fixtures* or requesting other variants to be tested. 125 | 126 | **Enhancements:** 127 | 128 | - The guessing function is ordered by global usage [usage of image file formats](https://w3techs.com/technologies/overview/image_format/all), but still keeping *png* as the first one. 129 | - Added support for *ico*, *jp2* (*jpeg 2000*) and the collection of *pnm* (*pbm*, *pgm* and *ppm*). 130 | - *ico* gets the dimensions of the largest image contained (not the first found). 131 | 132 | **Statistics:** 133 | 134 | - 54 TDD Tests. 135 | - Code coverage of 98.3%. 136 | - 10 image formats supported. 137 | 138 | **News:** 139 | 140 | - New minor version (`0.2.0`) due to the three new image formats supported. 141 | 142 | ## v0.1.1 (2016-08-12) 143 | 144 | **Enhancements:** 145 | 146 | - Warnings corrected (compiling). 147 | 148 | **Statistics:** 149 | 150 | - 34 TDD Tests. 151 | - Code coverage of 97.6%. 152 | - 7 image formats supported. 153 | 154 | **News:** 155 | 156 | - Initial release (published) + Docs (gh-pages). 157 | 158 | ## v0.1.0 (2016-08-11) 159 | 160 | **News:** 161 | 162 | - Initial release (pre-publish). 163 | -------------------------------------------------------------------------------- /CONTRIBUTORS.md: -------------------------------------------------------------------------------- 1 | # CONTRIBUTORS 2 | 3 | - Raúl (@rNoz) 4 | - Matthew Johnston (@warmwaffles) -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | # LICENSE 2 | 3 | MIT License 4 | 5 | Copyright (c) 2016 nozalr (Group4Layers®) 6 | 7 | Permission is hereby granted, free of charge, to any person obtaining a copy 8 | of this software and associated documentation files (the "Software"), to deal 9 | in the Software without restriction, including without limitation the rights 10 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 11 | copies of the Software, and to permit persons to whom the Software is 12 | furnished to do so, subject to the following conditions: 13 | 14 | The above copyright notice and this permission notice shall be included in all 15 | copies or substantial portions of the Software. 16 | 17 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 18 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 19 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 20 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 21 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 22 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 23 | SOFTWARE. 24 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # README 2 | 3 | # ExImageInfo 4 | 5 | [![v1.0.0](https://img.shields.io/badge/version-1.0.0-a1c43c.svg)](https://hex.pm/packages/ex_image_info) [![Elixir](https://img.shields.io/badge/made_in-elixir-9900cc.svg)](http://elixir-lang.org) [![Elixir ≥1.13](https://img.shields.io/badge/-≥1.13-9900cc.svg?logo=Elixir)](http://elixir-lang.org) [![License](https://img.shields.io/badge/license-MIT-blue.svg)](https://raw.githubusercontent.com/Group4Layers/ex_image_info/master/LICENSE.md) [![Coverage](https://img.shields.io/badge/coverage-98.4%25-green.svg)](https://github.com/Group4Layers/ex_image_info) [![Tests](https://img.shields.io/badge/tests-79%2F79-green.svg)](https://github.com/Group4Layers/ex_image_info) 6 | 7 | ExImageInfo is an Elixir library to parse images (binaries) and get the dimensions (size), detected mime-type and overall validity for a set of image formats. It is the fastest and supports multiple formats. 8 | 9 | Status: [![Elixir 1.18 / OTP 27](https://github.com/Group4Layers/ex_image_info/actions/workflows/build_elixir_latest.yml/badge.svg)](https://github.com/Group4Layers/ex_image_info/actions/workflows/build_elixir_latest.yml) [![Elixir 1.16 / OTP 26](https://github.com/Group4Layers/ex_image_info/actions/workflows/build_elixir_recent.yml/badge.svg)](https://github.com/Group4Layers/ex_image_info/actions/workflows/build_elixir_recent.yml) [![Elixir 1.14 / OTP 25](https://github.com/Group4Layers/ex_image_info/actions/workflows/build_elixir_old.yml/badge.svg)](https://github.com/Group4Layers/ex_image_info/actions/workflows/build_elixir_old.yml) [![Coverage Status](https://coveralls.io/repos/github/Group4Layers/ex_image_info/badge.svg?branch=master)](https://coveralls.io/github/Group4Layers/ex_image_info?branch=master) [![Inline docs](http://inch-ci.org/github/Group4Layers/ex_image_info.svg)](http://inch-ci.org/github/Group4Layers/ex_image_info) 10 | 11 | ### [GitHub repo](https://github.com/Group4Layers/ex_image_info) · [Docs](https://hexdocs.pm/ex_image_info/ExImageInfo.html) · [Hex.pm package](https://hex.pm/packages/ex_image_info) 12 | 13 | ## Table of Contents 14 | 15 | 1. [Description](#description) 16 | 1. [Installation](#installation) 17 | 1. [Examples](#examples) 18 | 1. [Benchmarks](#benchmarks) 19 | 1. [Design decisions](#design-decisions) 20 | 1. [Acknowledgments](#acknowledgments) 21 | 1. [Author](#author) 22 | 1. [Contributors](#contributors) 23 | 1. [ChangeLog](#changelog) 24 | 1. [License](#license) 25 | 26 | ## Description 27 | 28 | Main module that checks and gets if a binary seems to be an image (specific 29 | format), the mime-type (and variant detected) and the dimensions of the image 30 | (based on the type). 31 | 32 | It has convention functions to guess the type of an image by trying the formats 33 | supported by the library. 34 | 35 | ### Main features 36 | 37 | - Check the validity of binary by providing a specific image format*. 38 | - Guess the validity of an image*. 39 | - Get the mime-type and variant type by providing a specific format. 40 | - Guess the mime-type and variant type of an image. 41 | - Get the dimensions of an image by providing a specific format. 42 | - Guess the dimensions of an image. 43 | 44 | *Note: both cases as a general overview (partially checked). 45 | 46 | ### Formats 47 | 48 | Supported formats (image type to be parsed as): 49 | - `:bmp` 50 | - `:gif` 51 | - `:ico` 52 | - `:jpeg` (`:jpg` alias since `v0.2.3`) 53 | - `:jp2` 54 | - `:png` 55 | - `:pnm` 56 | - `:psd` 57 | - `:tiff` 58 | - `:webp` 59 | - `:avif` 60 | - `:heic` 61 | - `:heif` 62 | 63 | ## Mime-types and Variants 64 | 65 | The image variant type is an invented string to identify the 66 | type of format recognized by this library (more specific than the 67 | mime-type). 68 | 69 | Each mime-type can be linked to at least one variant type: 70 | 71 | | mime-type | variant type | description | since version | 72 | |---------------------------|--------------|--------------------|---------------| 73 | | `image/avif` | `AVIF` | | v1.0.0 | 74 | | `image/avif-sequence` | `AVIFS` | | v1.0.0 | 75 | | `image/bmp` | `BMP` | | | 76 | | `image/gif` | `GIF87a` | 87a gif spec | | 77 | | `image/gif` | `GIF89a` | 89a gif spec | | 78 | | `image/heic` | `HEIC` | | v1.0.0 | 79 | | `image/heic-sequence` | `HEICS` | | v1.0.0 | 80 | | `image/heif` | `HEIF` | | v1.0.0 | 81 | | `image/heif-sequence` | `HEIFS` | | v1.0.0 | 82 | | `image/x-icon` | `ICO` | | v0.2.0 | 83 | | `image/jp2` | `JP2` | JPEG2000 | v0.2.0 | 84 | | `image/jpeg` | `baseJPEG` | baseline JPEG | | 85 | | `image/jpeg` | `progJPEG` | progressive JPEG | | 86 | | `image/png` | `PNG` | | | 87 | | `image/x-portable-anymap` | `PNMpbm` | Portable BitMap | v0.2.0 | 88 | | `image/x-portable-anymap` | `PNMpgm` | Portable GrayMap | v0.2.0 | 89 | | `image/x-portable-anymap` | `PNMppm` | Portable PixMap | v0.2.0 | 90 | | `image/psd` | `PSD` | | | 91 | | `image/tiff` | `TIFFII` | II variant | | 92 | | `image/tiff` | `TIFFMM` | MM variant | | 93 | | `image/webp` | `webpVP8` | lossy | | 94 | | `image/webp` | `webpVP8L` | lossless | | 95 | | `image/webp` | `webpVP8X` | animated | v0.2.4 | 96 | 97 | The variant type is created just to provide a bit more of information for every image format (if applicable). 98 | If version is empty, it means that it was supported since the initial release. 99 | 100 | Formats (maybe) containing multiple images: 101 | - `:ico` returns the dimensions of the largest image found. 102 | - `:heif`, `:heic` and `:avif` return the dimensions of the main image being selected (`primary_box`). 103 | 104 | The guessing functions try to detect the format of the binary by testing every available type based on its 105 | global usage and current trends (popularity, [usage of image file formats](https://w3techs.com/technologies/overview/image_format/all)): 106 | - `jpeg`, `png`, `webp`, `avif`, `gif`, `heic`, `heif`, `bmp`, `ico`, `tiff`, `psd`, `jp2`, `pnm` 107 | 108 | **Warnings:** 109 | 110 | - Use with caution the formats *ico*, *jp2* and the family *pnm*. They are implemented without following 111 | other libraries (just reading the specs - sometimes working with old drafts like *jp2*). 112 | - ISOBMFF format (*heif*, *heic* and *avif*) is the most complex format being supported, with most parts 113 | being implemented following the specs and testing against binary streams manually produced. 114 | Please, use with caution and report any issue found. 115 | 116 | **Contributions:** you can support this library by providing more tests, image *fixtures* (like `image/heic-sequence`), 117 | increasing code coverage or extending support for other variants. 118 | 119 | 120 | ## Installation 121 | 122 | Add `ex_image_info` to your list of dependencies in `mix.exs`. 123 | 124 | From Hex: 125 | 126 | ```elixir 127 | def deps do 128 | [ 129 | # ... 130 | {:ex_image_info, "~> 1.0.0"}, 131 | ] 132 | end 133 | ``` 134 | 135 | Or GitHub: 136 | 137 | ```elixir 138 | def deps do 139 | [ 140 | # ... 141 | {:ex_image_info, github: "Group4Layers/ex_image_info"}, 142 | ] 143 | end 144 | ``` 145 | 146 | Then, use it: 147 | 148 | ```elixir 149 | require ExImageInfo 150 | # ExImageInfo.seems? ... 151 | ``` 152 | 153 | ## Examples 154 | 155 | The following examples are run with the latest version of the library under the next environment: 156 | 157 | ```elixir 158 | Erlang/OTP 19 [erts-8.0.2] [source] [64-bit] [smp:8:8] [async-threads:10] [hipe] [kernel-poll:false] 159 | 160 | Interactive Elixir (1.3.2) - press Ctrl+C to exit (type h() ENTER for help) 161 | iex(1)> 162 | ``` 163 | 164 | ### Feature `seems?` 165 | 166 | `89 50 4E 47 0D 0A 1A 0A` are the first 8 bytes in the `PNG` signature (`PNG\\r\\n0x1A\\n`). 167 | 168 | ```elixir 169 | iex(1)> ExImageInfo.seems? <<0x89504E470D0A1A0A::size(64)>>, :png 170 | true 171 | 172 | iex(2)> ExImageInfo.seems? <<0x89504E470D0A1A0A::size(64)>>, :webp 173 | false 174 | ``` 175 | 176 | `ExImageInfo.seems?/2` and `ExImageInfo.seems?/1` does not necessarily needs a real image (as it is shown in the previous example) because it just checks the signature of every file format. 177 | 178 | Usually it is used as: 179 | 180 | ```elixir 181 | iex(1)> ExImageInfo.seems? File.read!("path/to/image.gif"), :gif 182 | true 183 | 184 | iex(2)> maybe_png_binary |> ExImageInfo.seems? :png 185 | false 186 | ``` 187 | 188 | `38 42 50 53` are the first 4 bytes in the `PSD` signature (`8BPS`). 189 | 190 | ```elixir 191 | iex(1)> ExImageInfo.seems? <<0x38425053::size(32)>> 192 | :psd 193 | 194 | iex(2)> ExImageInfo.seems? <<0x384250::size(24)>> 195 | nil 196 | ``` 197 | 198 | `ExImageInfo.seems?/2` and `ExImageInfo.seems?/1` does not necessarily needs a real image (as it is shown in the previous example) because it just checks the signature of every file format. 199 | 200 | Usually it is used as: 201 | 202 | ```elixir 203 | iex(1)> ExImageInfo.seems? File.read!("path/to/image.unknown") 204 | :tiff 205 | 206 | iex(2)> webp_full_binary |> ExImageInfo.seems? 207 | :webp 208 | ``` 209 | 210 | ### Feature `type` 211 | 212 | `89 50 4E 47 0D 0A 1A 0A` are the first 8 bytes in the `PNG` signature (`PNG\\r\\n0x1A\\n`). 213 | 214 | ```elixir 215 | iex(1)> ExImageInfo.type <<0x89504E470D0A1A0A::size(64)>>, :png 216 | nil 217 | 218 | iex(2)> ExImageInfo.type <<"RIFF", 0::size(32), "WEBPVP8L", 0::size(32), 0x2F7AC07100358683B68D::size(80)>>, :webp 219 | {"image/webp", "webpVP8L"} 220 | ``` 221 | 222 | The signature part of a png it is now enough to get the type (it check also the IHDR field, just before the width and height). 223 | 224 | Usually it is used as: 225 | 226 | ```elixir 227 | iex(1)> ExImageInfo.type File.read!("path/to/image.gif"), :gif 228 | {"image/gif", "GIF87a"} 229 | 230 | iex(2)> maybe_png_binary |> ExImageInfo.type :png 231 | nil 232 | ``` 233 | 234 | The *guessed* version. 235 | 236 | ```elixir 237 | iex(1)> ExImageInfo.type <<0x38425053::size(32)>> 238 | {"image/psd", "PSD"} 239 | 240 | iex(2)> ExImageInfo.type <<0x384250::size(24)>> 241 | nil 242 | ``` 243 | 244 | Usually it is used as: 245 | 246 | ```elixir 247 | iex(1)> ExImageInfo.type File.read!("path/to/image.unknown") 248 | {"image/tiff", "TIFFMM"} 249 | 250 | iex(2)> webp_full_binary |> ExImageInfo.type 251 | {"image/webp", "webpVP8"} 252 | ``` 253 | 254 | ### Feature `info` 255 | 256 | `89 50 4E 47 0D 0A 1A 0A` are the first 8 bytes in the `PNG` signature (`PNG\\r\\n0x1A\\n`). 257 | 258 | ```elixir 259 | iex(1)> ExImageInfo.info <<0x89504E470D0A1A0A::size(64)>>, :png 260 | nil 261 | 262 | iex(2)> ExImageInfo.info <<"RIFF", 0::size(32), "WEBPVP8L", 0::size(32), 0x2F7AC07100358683B68D::size(80)>>, :webp 263 | {"image/webp", 123, 456, "webpVP8L"} 264 | ``` 265 | 266 | The signature part of a png it is now enough to get the type (it check also the IHDR field, just before the width and height). 267 | 268 | Usually it is used as: 269 | 270 | ```elixir 271 | iex(1)> ExImageInfo.info File.read!("path/to/image.gif"), :gif 272 | {"image/gif", 1920, 1080, "GIF87a"} 273 | 274 | iex(2)> maybe_png_binary |> ExImageInfo.info :png 275 | nil 276 | ``` 277 | 278 | ```elixir 279 | iex(1)> ExImageInfo.info <<0x38425053::size(32)>> 280 | nil 281 | 282 | iex(2)> ExImageInfo.info <<0x38425053::size(32), 0::size(80), 10::size(32), 12::size(32)>> 283 | {"image/psd", 12, 10, "PSD"} 284 | ``` 285 | 286 | Usually it is used as: 287 | 288 | ```elixir 289 | iex(1)> ExImageInfo.info File.read!("path/to/image.unknown") 290 | {"image/tiff", 128, 256, "TIFFMM"} 291 | 292 | iex(2)> webp_full_binary |> ExImageInfo.info 293 | {"image/webp", 20, 100, "webpVP8"} 294 | ``` 295 | 296 | ## Benchmarks 297 | 298 | Group4Layers developed the fastest elixir library to obtain the dimensions of the images (binary data parsed). Also, it excels supporting the maximum number of image formats. All without dependencies. 299 | 300 | ![ExImageInfo Benchmarks](assets/ex_image_info_benchmarks.png) 301 | 302 | (The image wouldn't be included in the package). 303 | 304 | ## Design decisions 305 | 306 | ### Why `seems?` and not `magic?` or `signature?`? 307 | 308 | Because for some formats it is enough with the [*magic 309 | number*](https://en.wikipedia.org/wiki/Magic_number_(programming)) or the 310 | signature to get the type (image format that "starts" correctly), but in other 311 | cases it is an algorithm a bit more complex to see if the binary seems 312 | correct. Therefore, *seems* it is more general (than getting the *magic 313 | number*) and it will provide a "quick overview" of the validity of the 314 | binary. 315 | 316 | ### Why returning the mime-type and variant type when getting the dimensions (`info`)? 317 | 318 | Because both types (variant if applicable) are necessary to obtain the width and height 319 | of the binary for a specific format. In case it is required both the type (and variant) and the dimensions it is not necessary to call two functions (and re-parse part or completely the binary). Therefore, to get the dimensions it is obtained the types and all the information is returned in one step. 320 | 321 | ### Renamed from ExImageSize to ExImageInfo 322 | 323 | Although it has been released since the very first version with the name ExImageInfo, 324 | this library was previously known as ExImageSize, but it is preferable to have a name 325 | less restricted. Nowadays it can get information about the type and the dimensions (size), 326 | but in a future it could increase the amount of info to extract from an image. 327 | 328 | ## Acknowledgments 329 | 330 | This idea comes from libraries that I have used in other platforms and/or languages. Algorithms and some concepts are picked and based on parts of the following: 331 | - image-size (JavaScript) - *Aditya Yadav* 332 | - imagesize (Ruby) - *Keisuke Minami* 333 | - fastimage (Ruby) - *Stephen Sykes* 334 | 335 | Thanks to them. 336 | 337 | ## Author 338 | 339 | nozalr (Group4Layers®). 340 | 341 | ## Contributors 342 | 343 | See [CONTRIBUTORS](contributors.html) for more information. 344 | 345 | *GitHub readers (repo, no docs): [CONTRIBUTORS.md](CONTRIBUTORS.md).* 346 | 347 | ## ChangeLog 348 | 349 | See [CHANGELOG](changelog.html) for more information. 350 | 351 | *GitHub readers (repo, no docs): [CHANGELOG.md](CHANGELOG.md).* 352 | 353 | ## License 354 | 355 | ExImageInfo source code is released under the MIT License. 356 | 357 | See [LICENSE](license.html) for more information. 358 | 359 | *GitHub readers (repo, no docs): [LICENSE.md](LICENSE.md).* 360 | -------------------------------------------------------------------------------- /VERSION: -------------------------------------------------------------------------------- 1 | 1.0.0 2 | -------------------------------------------------------------------------------- /assets/ex_image_info_benchmarks.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Group4Layers/ex_image_info/f9c8315ae562372c08c20e883683057782ecca5a/assets/ex_image_info_benchmarks.png -------------------------------------------------------------------------------- /lib/ex_image_info.ex: -------------------------------------------------------------------------------- 1 | defmodule ExImageInfo do 2 | @moduledoc """ 3 | ExImageInfo is an Elixir library to parse images (binaries) and get the dimensions (size), 4 | detected mime-type and overall validity for a set of image formats. Main module to parse a 5 | binary and get if it seems to be an image (validity), the mime-type (and variant detected) 6 | and the dimensions of the image, based on a specific image format. 7 | 8 | It has convention functions to guess the type of an image 9 | by trying the formats supported by the library. 10 | 11 | ## Main features 12 | 13 | - Check the validity of binary by providing a specific image format*. 14 | - Guess the validity of an image*. 15 | - Get the mime-type and variant type by providing a specific format. 16 | - Guess the mime-type and variant type of an image. 17 | - Get the dimensions of an image by providing a specific format. 18 | - Guess the dimensions of an image. 19 | 20 | *Note: both cases as a general overview (partially checked). 21 | 22 | ## Formats 23 | 24 | Supported formats (image type to be parsed as): 25 | - `:bmp` 26 | - `:gif` 27 | - `:ico` 28 | - `:jpeg` (`:jpg` alias since `v0.2.3`) 29 | - `:jp2` 30 | - `:png` 31 | - `:pnm` 32 | - `:psd` 33 | - `:tiff` 34 | - `:webp` 35 | - `:avif` 36 | - `:heic` 37 | - `:heif` 38 | 39 | ## Mime-types and Variants 40 | 41 | The image variant type is an invented string to identify the 42 | type of format recognized by this library (more specific than the 43 | mime-type). 44 | 45 | Each mime-type can be linked to at least one variant type: 46 | 47 | | mime-type | variant type | description | since version | 48 | |---------------------------|--------------|--------------------|---------------| 49 | | `image/avif` | `AVIF` | | v1.0.0 | 50 | | `image/avif-sequence` | `AVIFS` | | v1.0.0 | 51 | | `image/bmp` | `BMP` | | | 52 | | `image/gif` | `GIF87a` | 87a gif spec | | 53 | | `image/gif` | `GIF89a` | 89a gif spec | | 54 | | `image/heic` | `HEIC` | | v1.0.0 | 55 | | `image/heic-sequence` | `HEICS` | | v1.0.0 | 56 | | `image/heif` | `HEIF` | | v1.0.0 | 57 | | `image/heif-sequence` | `HEIFS` | | v1.0.0 | 58 | | `image/x-icon` | `ICO` | | v0.2.0 | 59 | | `image/jp2` | `JP2` | JPEG2000 | v0.2.0 | 60 | | `image/jpeg` | `baseJPEG` | baseline JPEG | | 61 | | `image/jpeg` | `progJPEG` | progressive JPEG | | 62 | | `image/png` | `PNG` | | | 63 | | `image/x-portable-anymap` | `PNMpbm` | Portable BitMap | v0.2.0 | 64 | | `image/x-portable-anymap` | `PNMpgm` | Portable GrayMap | v0.2.0 | 65 | | `image/x-portable-anymap` | `PNMppm` | Portable PixMap | v0.2.0 | 66 | | `image/psd` | `PSD` | | | 67 | | `image/tiff` | `TIFFII` | II variant | | 68 | | `image/tiff` | `TIFFMM` | MM variant | | 69 | | `image/webp` | `webpVP8` | lossy | | 70 | | `image/webp` | `webpVP8L` | lossless | | 71 | | `image/webp` | `webpVP8X` | animated | v0.2.4 | 72 | 73 | The variant type is created just to provide a bit more of information for every image format (if applicable). 74 | If version is empty, it means that it was supported since the initial release. 75 | 76 | Formats (maybe) containing multiple images: 77 | - `:ico` returns the dimensions of the largest image found. 78 | - `:heif`, `:heic` and `:avif` return the dimensions of the main image being selected (`primary_box`). 79 | 80 | The guessing functions try to detect the format of the binary by testing every available type based on its 81 | global usage and current trends (popularity, [usage of image file formats](https://w3techs.com/technologies/overview/image_format/all)): 82 | - `jpeg`, `png`, `webp`, `avif`, `gif`, `heic`, `heif`, `bmp`, `ico`, `tiff`, `psd`, `jp2`, `pnm` 83 | 84 | **Warnings:** 85 | 86 | - Use with caution the formats *ico*, *jp2* and the family *pnm*. They are implemented without following 87 | other libraries (just reading the specs - sometimes working with old drafts like *jp2*). 88 | - ISOBMFF format (*heif*, *heic* and *avif*) is the most complex format being supported, with most parts 89 | being implemented following the specs and testing against binary streams manually produced. 90 | Please, use with caution and report any issue found. 91 | 92 | **Contributions:** you can support this library by providing more tests, image *fixtures* (like `image/heic-sequence`), 93 | increasing code coverage or extending support for other variants. 94 | """ 95 | alias ExImageInfo.Types.AVIF 96 | alias ExImageInfo.Types.BMP 97 | alias ExImageInfo.Types.GIF 98 | alias ExImageInfo.Types.HEIC 99 | alias ExImageInfo.Types.HEIF 100 | alias ExImageInfo.Types.ICO 101 | alias ExImageInfo.Types.JP2 102 | alias ExImageInfo.Types.JPEG 103 | alias ExImageInfo.Types.PNG 104 | alias ExImageInfo.Types.PNM 105 | alias ExImageInfo.Types.PSD 106 | alias ExImageInfo.Types.TIFF 107 | alias ExImageInfo.Types.WEBP 108 | 109 | # Guessing function ordered by global usage 110 | # https://w3techs.com/technologies/overview/image_format/all 111 | # Balancing speed of detection and frequency of encounters in real-world scenarios (popularity) 112 | @types [ 113 | :jpeg, 114 | :png, 115 | :webp, 116 | :avif, 117 | :gif, 118 | :heic, 119 | :heif, 120 | :bmp, 121 | :ico, 122 | :tiff, 123 | :psd, 124 | :jp2, 125 | :pnm 126 | ] 127 | 128 | @typedoc "Supported image format types." 129 | @type image_format :: 130 | :jpeg 131 | | :jpg 132 | | :png 133 | | :webp 134 | | :avif 135 | | :gif 136 | | :heic 137 | | :heif 138 | | :bmp 139 | | :ico 140 | | :tiff 141 | | :psd 142 | | :jp2 143 | | :pnm 144 | 145 | @doc """ 146 | Detects if the given binary seems to be in the given image format. 147 | 148 | Valid [formats](#module-formats) to be used. 149 | 150 | Returns `true` if the binary seems to be the format specified, `false` if it 151 | is not, and `nil` if the type is unsupported. 152 | 153 | ## Examples 154 | 155 | `89 50 4E 47 0D 0A 1A 0A` are the first 8 bytes in the `PNG` signature (`PNG\\r\\n0x1A\\n`). 156 | 157 | iex> ExImageInfo.seems? <<0x89504E470D0A1A0A::size(64)>>, :png 158 | true 159 | iex> ExImageInfo.seems? <<0x89504E470D0A1A0A::size(64)>>, :webp 160 | false 161 | 162 | `ExImageInfo.seems?/2` and `ExImageInfo.seems?/1` does not necessarily needs a real image 163 | (as it is shown in the previous example) because it just checks the signature of every file format. 164 | 165 | Usually it is used as: 166 | 167 | ExImageInfo.seems? File.read!("path/to/image.gif"), :gif 168 | # true 169 | 170 | maybe_png_binary |> ExImageInfo.seems? :png 171 | # false 172 | """ 173 | @spec seems?(binary, format :: image_format()) :: boolean 174 | def seems?(binary, format) 175 | def seems?(binary, :png), do: PNG.seems?(binary) 176 | def seems?(binary, :gif), do: GIF.seems?(binary) 177 | def seems?(binary, :jpeg), do: JPEG.seems?(binary) 178 | def seems?(binary, :jpg), do: JPEG.seems?(binary) 179 | def seems?(binary, :bmp), do: BMP.seems?(binary) 180 | def seems?(binary, :tiff), do: TIFF.seems?(binary) 181 | def seems?(binary, :webp), do: WEBP.seems?(binary) 182 | def seems?(binary, :psd), do: PSD.seems?(binary) 183 | def seems?(binary, :jp2), do: JP2.seems?(binary) 184 | def seems?(binary, :pnm), do: PNM.seems?(binary) 185 | def seems?(binary, :ico), do: ICO.seems?(binary) 186 | def seems?(binary, :avif), do: AVIF.seems?(binary) 187 | def seems?(binary, :heic), do: HEIC.seems?(binary) 188 | def seems?(binary, :heif), do: HEIF.seems?(binary) 189 | 190 | @doc """ 191 | Detects the image format that seems to be the given binary (*guessed* version of `ExImageInfo.seems?/2`). 192 | 193 | Returns the valid [format](#module-formats) (atom) if it matches, `nil` otherwise. 194 | 195 | ## Examples 196 | 197 | `38 42 50 53` are the first 4 bytes in the `PSD` signature (`8BPS`). 198 | 199 | iex> ExImageInfo.seems? <<0x38425053::size(32)>> 200 | :psd 201 | iex> ExImageInfo.seems? <<0x384250::size(24)>> 202 | nil 203 | 204 | `ExImageInfo.seems?/2` and `ExImageInfo.seems?/1` does not necessarily needs a real image 205 | (as it is shown in the previous example) because it just checks the signature of every file format. 206 | 207 | Usually it is used as: 208 | 209 | ExImageInfo.seems? File.read!("path/to/image.unknown") 210 | # :tiff 211 | 212 | webp_full_binary |> ExImageInfo.seems? 213 | # :webp 214 | """ 215 | @spec seems?(binary) :: image_format() | nil 216 | def seems?(binary), do: try_seems?(binary, @types) 217 | 218 | @doc """ 219 | Gets the mime-type and variant type for the given image format and binary. 220 | 221 | Possible [Mime-types and Variants](#module-mime-types-and-variants) to be returned. 222 | 223 | Valid [formats](#module-formats) to be used. 224 | 225 | Returns a 2-item tuple with the mime-type and the variant type when the binary matches, `nil` otherwise. 226 | 227 | ## Examples 228 | 229 | `89 50 4E 47 0D 0A 1A 0A` are the first 8 bytes in the `PNG` signature (`PNG\\r\\n0x1A\\n`). 230 | 231 | iex> ExImageInfo.type <<0x89504E470D0A1A0A::size(64)>>, :png 232 | nil 233 | iex> ExImageInfo.type <<"RIFF", 0::size(32), "WEBPVP8L", 0::size(32), 0x2F7AC07100358683B68D::size(80)>>, :webp 234 | {"image/webp", "webpVP8L"} 235 | 236 | The signature part of a png it is now enough to get the type (it check also the IHDR field, just before the width and height). 237 | 238 | Usually it is used as: 239 | 240 | ExImageInfo.type File.read!("path/to/image.gif"), :gif 241 | # {"image/gif", "GIF87a"} 242 | 243 | maybe_png_binary |> ExImageInfo.type :png 244 | # nil 245 | """ 246 | @spec type(binary, format :: image_format()) :: 247 | {mimetype :: String.t(), variant :: String.t()} | nil 248 | def type(binary, format) 249 | def type(binary, :png), do: PNG.type(binary) 250 | def type(binary, :gif), do: GIF.type(binary) 251 | def type(binary, :jpeg), do: JPEG.type(binary) 252 | def type(binary, :jpg), do: JPEG.type(binary) 253 | def type(binary, :bmp), do: BMP.type(binary) 254 | def type(binary, :tiff), do: TIFF.type(binary) 255 | def type(binary, :webp), do: WEBP.type(binary) 256 | def type(binary, :psd), do: PSD.type(binary) 257 | def type(binary, :jp2), do: JP2.type(binary) 258 | def type(binary, :pnm), do: PNM.type(binary) 259 | def type(binary, :ico), do: ICO.type(binary) 260 | def type(binary, :avif), do: AVIF.type(binary) 261 | def type(binary, :heic), do: HEIC.type(binary) 262 | def type(binary, :heif), do: HEIF.type(binary) 263 | 264 | @doc """ 265 | Gets the mime-type and variant type for the given image binary (*guessed* version of `ExImageInfo.type/2`). 266 | 267 | Possible [Mime-types and Variants](#module-mime-types-and-variants) to be returned. 268 | 269 | Returns a 2-item tuple with the mime-type and the variant type when the binary matches, `nil` otherwise. 270 | 271 | ## Examples 272 | 273 | iex> ExImageInfo.type <<0x38425053::size(32)>> 274 | {"image/psd", "PSD"} 275 | iex> ExImageInfo.type <<0x384250::size(24)>> 276 | nil 277 | 278 | Usually it is used as: 279 | 280 | ExImageInfo.type File.read!("path/to/image.unknown") 281 | # {"image/tiff", "TIFFMM"} 282 | 283 | webp_full_binary |> ExImageInfo.type 284 | # {"image/webp", "webpVP8"} 285 | """ 286 | @spec type(binary) :: {mimetype :: String.t(), variant :: String.t()} | nil 287 | def type(binary), do: try_type(binary, @types) 288 | 289 | @doc """ 290 | Gets the mime-type, variant-type and dimensions (width, height) for the given image format and binary. 291 | 292 | Possible [Mime-types and Variants](#module-mime-types-and-variants) to be returned. 293 | 294 | Valid [formats](#module-formats) to be used. 295 | 296 | Returns a 4-item tuple with the mime-type, width, height and the variant type when the binary matches, `nil` otherwise. 297 | 298 | ## Examples 299 | 300 | `89 50 4E 47 0D 0A 1A 0A` are the first 8 bytes in the `PNG` signature (`PNG\\r\\n0x1A\\n`). 301 | 302 | iex> ExImageInfo.info <<0x89504E470D0A1A0A::size(64)>>, :png 303 | nil 304 | iex> ExImageInfo.info <<"RIFF", 0::size(32), "WEBPVP8L", 0::size(32), 0x2F7AC07100358683B68D::size(80)>>, :webp 305 | {"image/webp", 123, 456, "webpVP8L"} 306 | 307 | The signature part of a png it is now enough to get the type (it check also the IHDR field, just before the width and height). 308 | 309 | Usually it is used as: 310 | 311 | ExImageInfo.info File.read!("path/to/image.gif"), :gif 312 | # {"image/gif", 1920, 1080, "GIF87a"} 313 | 314 | maybe_png_binary |> ExImageInfo.info :png 315 | # nil 316 | 317 | If a binary is malformed, it returns `nil`, even if other calls like `type` or `seems?` return valid types. 318 | 319 | {ExImageInfo.type(malformed_heif_binary, :heif), ExImageInfo.info(malformed_heif_binary, :heif)} 320 | # {{"image/heif", "HEIF"}, nil} 321 | """ 322 | @spec info(binary, format :: image_format()) :: 323 | {mimetype :: String.t(), width :: integer(), height :: integer(), 324 | variant :: String.t()} 325 | | nil 326 | def info(binary, format) 327 | def info(binary, :png), do: PNG.info(binary) 328 | def info(binary, :gif), do: GIF.info(binary) 329 | def info(binary, :jpeg), do: JPEG.info(binary) 330 | def info(binary, :jpg), do: JPEG.info(binary) 331 | def info(binary, :bmp), do: BMP.info(binary) 332 | def info(binary, :tiff), do: TIFF.info(binary) 333 | def info(binary, :webp), do: WEBP.info(binary) 334 | def info(binary, :psd), do: PSD.info(binary) 335 | def info(binary, :jp2), do: JP2.info(binary) 336 | def info(binary, :pnm), do: PNM.info(binary) 337 | def info(binary, :ico), do: ICO.info(binary) 338 | def info(binary, :avif), do: AVIF.info(binary) 339 | def info(binary, :heic), do: HEIC.info(binary) 340 | def info(binary, :heif), do: HEIF.info(binary) 341 | 342 | @doc """ 343 | Gets the mime-type, variant-type and dimensions (width, height) for the given image binary 344 | (*guessed* version of `ExImageInfo.info/2`). 345 | 346 | Possible [Mime-types and Variants](#module-mime-types-and-variants) to be returned. 347 | 348 | Returns a 4-item tuple with the mime-type, width, height and the variant type when the binary matches, `nil` otherwise. 349 | 350 | ## Examples 351 | 352 | iex> ExImageInfo.info <<0x38425053::size(32)>> 353 | nil 354 | iex> ExImageInfo.info <<0x38425053::size(32), 0::size(80), 10::size(32), 12::size(32)>> 355 | {"image/psd", 12, 10, "PSD"} 356 | 357 | Usually it is used as: 358 | 359 | ExImageInfo.info File.read!("path/to/image.unknown") 360 | # {"image/tiff", 128, 256, "TIFFMM"} 361 | 362 | webp_full_binary |> ExImageInfo.info 363 | # {"image/webp", 20, 100, "webpVP8"} 364 | """ 365 | @spec info(binary) :: 366 | {mimetype :: String.t(), width :: integer(), height :: integer(), 367 | variant :: String.t()} 368 | | nil 369 | def info(binary), do: try_info(binary, @types) 370 | 371 | defp try_seems?(_binary, []), do: nil 372 | 373 | defp try_seems?(binary, [type | types]) do 374 | if seems?(binary, type), do: type, else: try_seems?(binary, types) 375 | end 376 | 377 | defp try_type(_binary, []), do: nil 378 | 379 | defp try_type(binary, [type | types]) do 380 | case type(binary, type) do 381 | type_t when is_tuple(type_t) -> type_t 382 | _ -> try_type(binary, types) 383 | end 384 | end 385 | 386 | defp try_info(_binary, []), do: nil 387 | 388 | defp try_info(binary, [type | types]) do 389 | case info(binary, type) do 390 | info_t when is_tuple(info_t) -> info_t 391 | _ -> try_info(binary, types) 392 | end 393 | end 394 | end 395 | -------------------------------------------------------------------------------- /lib/ex_image_info/detector.ex: -------------------------------------------------------------------------------- 1 | defmodule ExImageInfo.Detector do 2 | @moduledoc false 3 | 4 | @callback info(binary) :: 5 | {mimetype :: String.t(), width :: integer(), height :: integer(), 6 | variant :: String.t()} 7 | | nil 8 | @callback type(binary) :: {mimetype :: String.t(), variant :: String.t()} | nil 9 | @callback seems?(binary) :: boolean() 10 | end 11 | -------------------------------------------------------------------------------- /lib/ex_image_info/parsers/isobmff.ex: -------------------------------------------------------------------------------- 1 | defmodule ExImageInfo.Parsers.ISOBMFF do 2 | @moduledoc "HEIF, HEIC and AVIF image format parser" 3 | 4 | # There are code regions that cannot be tested as no fixtures/images to be validated against. 5 | # Returning nil to avoid failing, but ideally this format should have more image files. 6 | @unimplemented nil 7 | @malformed nil 8 | 9 | defmodule State do 10 | @moduledoc false 11 | defstruct index: 0, 12 | rotation: 0, 13 | primary_box: nil, 14 | ipma_boxes: [], 15 | ispe_boxes: [], 16 | readable_bytes: nil 17 | 18 | @type t :: %__MODULE__{ 19 | rotation: integer(), 20 | index: integer(), 21 | primary_box: integer() | nil, 22 | ipma_boxes: list(), 23 | ispe_boxes: list(), 24 | readable_bytes: integer() | nil 25 | } 26 | 27 | def readable_bytes?(state) do 28 | state.readable_bytes == nil || state.readable_bytes > 0 29 | end 30 | 31 | def update_readable_bytes(state, size) when size < 0 do 32 | if state.readable_bytes == nil do 33 | state 34 | else 35 | %{state | readable_bytes: state.readable_bytes + size} 36 | end 37 | end 38 | 39 | def update_readable_bytes(state, readable_bytes), 40 | do: %{state | readable_bytes: readable_bytes} 41 | 42 | def update_index(state, value \\ nil) do 43 | value = if value == nil, do: state.index + 1, else: value 44 | %{state | index: value} 45 | end 46 | 47 | def ispe_box(state, width, height) do 48 | %{state | ispe_boxes: [{state.index, {width, height}} | state.ispe_boxes]} 49 | end 50 | 51 | def ipma_box(state, id, property_idx) do 52 | %{state | ipma_boxes: [{id, property_idx} | state.ipma_boxes]} 53 | end 54 | end 55 | 56 | @doc """ 57 | Parses the binary to get the image size (ISOBMFF spec) 58 | """ 59 | def parse_image_size(<>), do: parse_box(bin, %State{}) 60 | 61 | defp parse_box(<>, state) do 62 | if State.readable_bytes?(state) do 63 | case read_box_header(rest, state) do 64 | {type, size, rest, state} -> handle_box(type, size, rest, state) 65 | malformed -> malformed 66 | end 67 | else 68 | @malformed 69 | end 70 | end 71 | 72 | defp read_box_header( 73 | <<0x01::size(32), type::binary-size(4), extended_size::size(64), 74 | rest::binary>>, 75 | state 76 | ) do 77 | # 4 bytes for size field, 4 bytes for type field, 8 bytes for extended size 78 | header_size = 4 + 4 + 8 79 | state = State.update_readable_bytes(state, -header_size) 80 | 81 | box_size = extended_size - header_size 82 | {type, box_size, rest, state} 83 | end 84 | 85 | defp read_box_header(<>, state) do 86 | # 4 bytes for size field, 4 bytes for type field 87 | header_size = 4 + 4 88 | state = State.update_readable_bytes(state, -header_size) 89 | 90 | box_size = size - header_size 91 | {type, box_size, rest, state} 92 | end 93 | 94 | defp read_box_header(_malformed_binary, _state), do: @malformed 95 | 96 | defp handle_box("ftyp", size, <>, state) do 97 | case rest do 98 | <<_::binary-size(size), rest::binary>> -> 99 | state = 100 | state 101 | |> State.update_readable_bytes(-size) 102 | |> State.update_index() 103 | 104 | parse_box(rest, state) 105 | 106 | _ -> 107 | @malformed 108 | end 109 | end 110 | 111 | defp handle_box("meta", size, <<_rest::binary>>, _state) when size < 4, 112 | do: @unimplemented 113 | 114 | defp handle_box("meta", size, <<_::binary-size(4), rest::binary>>, state) do 115 | max = size - 4 116 | 117 | state = 118 | state 119 | |> State.update_readable_bytes(max) 120 | |> State.update_index(0) 121 | 122 | with %State{primary_box: pbox} = state <- parse_box(rest, state), 123 | # not pitm box 124 | false <- is_nil(pbox) do 125 | primary_indices = 126 | state.ipma_boxes 127 | |> Stream.filter(fn {id, _} -> id == pbox end) 128 | |> Stream.map(&elem(&1, 1)) 129 | |> Enum.reverse() 130 | 131 | ispe_box = 132 | state.ispe_boxes 133 | |> Enum.reverse() 134 | |> Enum.find(fn {idx, _} -> idx in primary_indices end) 135 | 136 | case ispe_box do 137 | {_idx, {width, height}} -> 138 | if state.rotation in [90, 270] do 139 | {height, width} 140 | else 141 | {width, height} 142 | end 143 | 144 | _ -> 145 | # wrong primary_box assignation 146 | @malformed 147 | end 148 | end 149 | end 150 | 151 | defp handle_box("hdlr", size, <>, state) when size >= 12 do 152 | case rest do 153 | <<_::binary-size(8), "pict", _::binary-size(size - 8 - 4), rest::binary>> -> 154 | state = 155 | state 156 | |> State.update_readable_bytes(-size) 157 | |> State.update_index() 158 | 159 | parse_box(rest, state) 160 | 161 | <<_::binary-size(size), _rest::binary>> -> 162 | @unimplemented 163 | 164 | _ -> 165 | @malformed 166 | end 167 | end 168 | 169 | defp handle_box("hdlr", _size, <<_rest::binary>>, _state), do: @unimplemented 170 | 171 | defp handle_box("pitm", size, <>, state) do 172 | case rest do 173 | <<_::binary-size(4), pbox::size(16), _::binary-size(size - 4 - 2), rest::binary>> -> 174 | state = 175 | %{state | primary_box: pbox} 176 | |> State.update_readable_bytes(-size) 177 | |> State.update_index() 178 | 179 | parse_box(rest, state) 180 | 181 | _ -> 182 | @malformed 183 | end 184 | end 185 | 186 | # box types found in all images tested: 187 | # 188 | # @skippable [ 189 | # "iloc", 190 | # "iinf", 191 | # "hvcC", 192 | # "clap", 193 | # "pixi", 194 | # "iref", 195 | # "dinf", 196 | # "colr", 197 | # "auxC", 198 | # "mdat", 199 | # # Mac Preview App - rotate - export as heic generates this box: 200 | # "clli", 201 | # # avif 202 | # "pasp", 203 | # "av1C" 204 | # ] 205 | # defp handle_box(skippable, size, <>, state) 206 | # when skippable in @skippable do 207 | # case rest do 208 | # <<_::binary-size(size), rest::binary>> -> 209 | # state = state |> State.update_readable_bytes(-size) |> State.update_index() 210 | # parse_box(rest, state) 211 | # 212 | # _ -> 213 | # @malformed 214 | # end 215 | # end 216 | # 217 | # to increase robustness, changing the spec to accept any type, moving to the bottom of `handle_box` functions 218 | 219 | @nestable ["iprp", "ipco"] 220 | defp handle_box(nestable, _size, <>, state) 221 | when nestable in @nestable do 222 | parse_box(rest, State.update_index(state, 0)) 223 | end 224 | 225 | defp handle_box("ispe", size, <>, state) when size >= 12 do 226 | case rest do 227 | <<_::binary-size(4), width::size(32), height::size(32), _::binary-size(size - 12), 228 | rest::binary>> -> 229 | state = 230 | state 231 | |> State.update_readable_bytes(-size) 232 | |> State.ispe_box(width, height) 233 | |> State.update_index() 234 | 235 | parse_box(rest, state) 236 | 237 | <<_::binary-size(size), _rest::binary>> -> 238 | @unimplemented 239 | 240 | _ -> 241 | @malformed 242 | end 243 | end 244 | 245 | defp handle_box("ispe", _size, <<_rest::binary>>, _state), do: @unimplemented 246 | 247 | defp handle_box( 248 | "ipma", 249 | _size, 250 | <<_::binary-size(3), flags3::size(8), entries::size(32), rest::binary>>, 251 | state 252 | ) do 253 | state = State.update_readable_bytes(state, -8) 254 | flags3? = Bitwise.band(flags3, 0b00000001) == 1 255 | 256 | {_rest, state} = 257 | Enum.reduce(0..(entries - 1)//1, {rest, state}, fn _i, {rest, state} -> 258 | read_ipma_entry(rest, flags3?, state) 259 | end) 260 | 261 | state 262 | end 263 | 264 | defp handle_box("irot", size, <>, state) do 265 | state = State.update_readable_bytes(state, -size) 266 | rotation = Bitwise.band(rotation, 0x3) * 90 267 | state = State.update_index(%{state | rotation: rotation}) 268 | parse_box(rest, state) 269 | end 270 | 271 | defp handle_box("jxlc", _size, <<_rest::binary>>, _state), do: @unimplemented 272 | 273 | defp handle_box(_skippable, size, <>, state) do 274 | case rest do 275 | <<_::binary-size(size), rest::binary>> -> 276 | state = state |> State.update_readable_bytes(-size) |> State.update_index() 277 | parse_box(rest, state) 278 | 279 | _ -> 280 | @malformed 281 | end 282 | end 283 | 284 | defp read_ipma_entry( 285 | <>, 286 | flags3?, 287 | state 288 | ) do 289 | state = State.update_readable_bytes(state, -3) 290 | 291 | Enum.reduce(0..(essen_count - 1)//1, {rest, state}, fn _i, {rest, state} -> 292 | read_ipma_essen_entry(id, rest, flags3?, state) 293 | end) 294 | end 295 | 296 | defp read_ipma_essen_entry( 297 | id, 298 | <>, 299 | true = _flags3?, 300 | state 301 | ) do 302 | state = State.update_readable_bytes(state, -2) 303 | property_idx = Bitwise.band(property_idx, 0x7F) 304 | property_idx = Bitwise.<<<(property_idx, 7) + property_idx_add 305 | {rest, State.ipma_box(state, id, property_idx - 1)} 306 | end 307 | 308 | defp read_ipma_essen_entry(id, <>, false, state) do 309 | state = State.update_readable_bytes(state, -1) 310 | property_idx = Bitwise.band(property_idx, 0x7F) 311 | {rest, State.ipma_box(state, id, property_idx - 1)} 312 | end 313 | end 314 | -------------------------------------------------------------------------------- /lib/ex_image_info/types/avif.ex: -------------------------------------------------------------------------------- 1 | defmodule ExImageInfo.Types.AVIF do 2 | @moduledoc false 3 | 4 | @behaviour ExImageInfo.Detector 5 | 6 | alias ExImageInfo.Parsers.ISOBMFF 7 | 8 | @signature_base "ftyp" 9 | 10 | @mime "image/avif" 11 | @ftype "AVIF" 12 | @brand "avif" 13 | 14 | @mime_seq "image/avif-sequence" 15 | @ftype_seq "AVIFS" 16 | @brand_seq "avis" 17 | 18 | def seems?(<<_::size(32), @signature_base, @brand, _::binary>>), do: true 19 | def seems?(<<_::size(32), @signature_base, @brand_seq, _::binary>>), do: true 20 | def seems?(_), do: false 21 | 22 | def info(<<_::size(32), @signature_base, @brand, _::binary>> = binary) do 23 | with {w, h} <- ISOBMFF.parse_image_size(binary) do 24 | {@mime, w, h, @ftype} 25 | end 26 | end 27 | 28 | def info(<<_::size(32), @signature_base, @brand_seq, _::binary>> = binary) do 29 | with {w, h} <- ISOBMFF.parse_image_size(binary) do 30 | {@mime_seq, w, h, @ftype_seq} 31 | end 32 | end 33 | 34 | def info(_), do: nil 35 | 36 | def type(<<_::size(32), @signature_base, @brand, _::binary>>), do: {@mime, @ftype} 37 | 38 | def type(<<_::size(32), @signature_base, @brand_seq, _::binary>>), 39 | do: {@mime_seq, @ftype_seq} 40 | 41 | def type(_), do: nil 42 | end 43 | -------------------------------------------------------------------------------- /lib/ex_image_info/types/bmp.ex: -------------------------------------------------------------------------------- 1 | defmodule ExImageInfo.Types.BMP do 2 | @moduledoc false 3 | 4 | @behaviour ExImageInfo.Detector 5 | 6 | @mime "image/bmp" 7 | @ftype "BMP" 8 | 9 | @signature <<"BM">> 10 | 11 | def seems?(<<@signature, _rest::binary>>), do: true 12 | def seems?(_), do: false 13 | 14 | def info( 15 | <<@signature, _::bytes-size(16), width::little-size(16), _::bytes-size(2), 16 | height::little-size(16), _rest::binary>> 17 | ), 18 | do: {@mime, width, height, @ftype} 19 | 20 | def info(_), do: nil 21 | 22 | def type(<<@signature, _rest::binary>>), do: {@mime, @ftype} 23 | def type(_), do: nil 24 | end 25 | -------------------------------------------------------------------------------- /lib/ex_image_info/types/gif.ex: -------------------------------------------------------------------------------- 1 | defmodule ExImageInfo.Types.GIF do 2 | @moduledoc false 3 | 4 | @behaviour ExImageInfo.Detector 5 | 6 | @mime "image/gif" 7 | @ftype89 "GIF89a" 8 | @ftype87 "GIF87a" 9 | 10 | @signature_87a <<"GIF87a">> 11 | @signature_89a <<"GIF89a">> 12 | 13 | def seems?(<<@signature_87a, _rest::binary>>), do: true 14 | def seems?(<<@signature_89a, _rest::binary>>), do: true 15 | def seems?(_), do: false 16 | 17 | def info( 18 | <<@signature_87a, width::little-size(16), height::little-size(16), 19 | _rest::binary>> 20 | ), 21 | do: {@mime, width, height, @ftype87} 22 | 23 | def info( 24 | <<@signature_89a, width::little-size(16), height::little-size(16), 25 | _rest::binary>> 26 | ), 27 | do: {@mime, width, height, @ftype89} 28 | 29 | def info(_), do: nil 30 | 31 | def type(<<@signature_89a, _rest::binary>>), do: {@mime, @ftype89} 32 | def type(<<@signature_87a, _rest::binary>>), do: {@mime, @ftype87} 33 | def type(_), do: nil 34 | end 35 | -------------------------------------------------------------------------------- /lib/ex_image_info/types/heic.ex: -------------------------------------------------------------------------------- 1 | defmodule ExImageInfo.Types.HEIC do 2 | @moduledoc false 3 | 4 | @behaviour ExImageInfo.Detector 5 | 6 | alias ExImageInfo.Parsers.ISOBMFF 7 | 8 | @signature_base "ftyp" 9 | 10 | @mime "image/heic" 11 | @ftype "HEIC" 12 | @brands ["heic", "heix", "heim", "heis"] 13 | 14 | @mime_seq "image/heic-sequence" 15 | @ftype_seq "HEICS" 16 | @brands_seq ["hevc", "hevx", "hevm", "hevs"] 17 | 18 | def seems?(<<_::size(32), @signature_base, brand::binary-size(4), _::binary>>) 19 | when brand in @brands, 20 | do: true 21 | 22 | def seems?(<<_::size(32), @signature_base, brand_seq::binary-size(4), _::binary>>) 23 | when brand_seq in @brands_seq, 24 | do: true 25 | 26 | def seems?(_), do: false 27 | 28 | def info(<<_::size(32), @signature_base, brand::binary-size(4), _::binary>> = binary) 29 | when brand in @brands do 30 | with {w, h} <- ISOBMFF.parse_image_size(binary) do 31 | {@mime, w, h, @ftype} 32 | end 33 | end 34 | 35 | def info( 36 | <<_::size(32), @signature_base, brand_seq::binary-size(4), _::binary>> = binary 37 | ) 38 | when brand_seq in @brands_seq do 39 | with {w, h} <- ISOBMFF.parse_image_size(binary) do 40 | {@mime_seq, w, h, @ftype_seq} 41 | end 42 | end 43 | 44 | def info(_), do: nil 45 | 46 | def type(<<_::size(32), @signature_base, brand::binary-size(4), _::binary>>) 47 | when brand in @brands, 48 | do: {@mime, @ftype} 49 | 50 | def type(<<_::size(32), @signature_base, brand_seq::binary-size(4), _::binary>>) 51 | when brand_seq in @brands_seq, 52 | do: {@mime_seq, @ftype_seq} 53 | 54 | def type(_), do: nil 55 | end 56 | -------------------------------------------------------------------------------- /lib/ex_image_info/types/heif.ex: -------------------------------------------------------------------------------- 1 | defmodule ExImageInfo.Types.HEIF do 2 | @moduledoc false 3 | 4 | @behaviour ExImageInfo.Detector 5 | 6 | alias ExImageInfo.Parsers.ISOBMFF 7 | 8 | @signature_base "ftyp" 9 | 10 | @mime "image/heif" 11 | @ftype "HEIF" 12 | @brand "mif1" 13 | 14 | @mime_seq "image/heif-sequence" 15 | @ftype_seq "HEIFS" 16 | @brand_seq "msf1" 17 | 18 | def seems?(<<_::size(32), @signature_base, @brand, _::binary>>), do: true 19 | def seems?(<<_::size(32), @signature_base, @brand_seq, _::binary>>), do: true 20 | def seems?(_), do: false 21 | 22 | def info(<<_::size(32), @signature_base, @brand, _::binary>> = binary) do 23 | with {w, h} <- ISOBMFF.parse_image_size(binary) do 24 | {@mime, w, h, @ftype} 25 | end 26 | end 27 | 28 | def info(<<_::size(32), @signature_base, @brand_seq, _::binary>> = binary) do 29 | with {w, h} <- ISOBMFF.parse_image_size(binary) do 30 | {@mime_seq, w, h, @ftype_seq} 31 | end 32 | end 33 | 34 | def info(_), do: nil 35 | 36 | def type(<<_::size(32), @signature_base, @brand, _::binary>>), do: {@mime, @ftype} 37 | 38 | def type(<<_::size(32), @signature_base, @brand_seq, _::binary>>), 39 | do: {@mime_seq, @ftype_seq} 40 | 41 | def type(_), do: nil 42 | end 43 | -------------------------------------------------------------------------------- /lib/ex_image_info/types/ico.ex: -------------------------------------------------------------------------------- 1 | defmodule ExImageInfo.Types.ICO do 2 | @moduledoc false 3 | 4 | @behaviour ExImageInfo.Detector 5 | # https://en.wikipedia.org/wiki/ICO_(file_format) 6 | 7 | @mime "image/x-icon" 8 | @ftype "ICO" 9 | 10 | # 0x0100 -> little endian -> 1 (.ICO), 2 (.CUR) 11 | @signature <<0::size(16), 0x01, 0x00>> 12 | 13 | def seems?(<<@signature, _rest::binary>>), do: true 14 | def seems?(_), do: false 15 | 16 | def info(<<@signature, num_images::little-size(16), rest::binary>>) do 17 | case parse(rest, num_images) do 18 | {w, h} -> {@mime, w, h, @ftype} 19 | _ -> nil 20 | end 21 | end 22 | 23 | def info(_), do: nil 24 | 25 | def type(<<@signature, _rest::binary>>), do: {@mime, @ftype} 26 | def type(_), do: nil 27 | 28 | defp parse(binary, num), do: parse(binary, num, {0, 0}) 29 | 30 | defp parse( 31 | <>, 32 | num, 33 | {wp, hp} = acc 34 | ) do 35 | w = if w == 0, do: 256, else: w 36 | h = if h == 0, do: 256, else: h 37 | sump = wp + hp 38 | sum = w + h 39 | acc = if sum > sump, do: {w, h}, else: acc 40 | # last one 41 | if num == 1 do 42 | acc 43 | else 44 | parse(rest, num - 1, acc) 45 | end 46 | end 47 | 48 | defp parse(_, _, _), do: nil 49 | end 50 | -------------------------------------------------------------------------------- /lib/ex_image_info/types/jp2.ex: -------------------------------------------------------------------------------- 1 | defmodule ExImageInfo.Types.JP2 do 2 | @moduledoc false 3 | 4 | @behaviour ExImageInfo.Detector 5 | # https://en.wikipedia.org/wiki/JPEG_2000 6 | # https://web.archive.org/web/20060518140251/http://www.jpeg.org/public/15444-1annexi.pdf 7 | 8 | @mime "image/jp2" 9 | @ftype "JP2" 10 | 11 | @signature <<0::size(24), 0x0C6A5020200D0A870A::size(72)>> 12 | @signature_ihdr <<"ihdr">> 13 | 14 | def seems?(<<@signature, _rest::binary>>), do: true 15 | def seems?(_), do: false 16 | 17 | def info( 18 | <<@signature, _::bytes-size(32), @signature_ihdr, h::size(32), w::size(32), 19 | _rest::binary>> 20 | ), 21 | do: {@mime, w, h, @ftype} 22 | 23 | def info(_), do: nil 24 | 25 | def type(<<@signature, _rest::binary>>), do: {@mime, @ftype} 26 | def type(_), do: nil 27 | end 28 | -------------------------------------------------------------------------------- /lib/ex_image_info/types/jpeg.ex: -------------------------------------------------------------------------------- 1 | defmodule ExImageInfo.Types.JPEG do 2 | @moduledoc false 3 | 4 | @behaviour ExImageInfo.Detector 5 | # https://en.wikipedia.org/wiki/JPEG 6 | 7 | @mime "image/jpeg" 8 | @ftype_base "baseJPEG" 9 | @ftype_prog "progJPEG" 10 | 11 | @signature <<0xFFD8::size(16)>> 12 | 13 | def seems?(<<@signature, _rest::binary>>), do: true 14 | def seems?(_), do: false 15 | 16 | def info(<<@signature, _::size(16), rest::binary>>), do: parse_jpeg(rest) 17 | def info(_), do: nil 18 | 19 | def type(bin) do 20 | case info(bin) do 21 | {mime, _, _, ftype} -> {mime, ftype} 22 | _ -> nil 23 | end 24 | end 25 | 26 | defp parse_jpeg(<>) do 27 | parse_jpeg_block(block_len, rest) 28 | end 29 | 30 | defp parse_jpeg(_), do: nil 31 | 32 | defp parse_jpeg_block(block_len, <>) do 33 | # bytes of block_len 34 | block_len = block_len - 2 35 | 36 | case rest do 37 | <<_::bytes-size(block_len), 0xFF, sof::size(8), next::binary>> -> 38 | parse_jpeg_sof(sof, next) 39 | 40 | _ -> 41 | nil 42 | end 43 | end 44 | 45 | defp parse_jpeg_block(_, _), do: nil 46 | 47 | defp parse_jpeg_sof(0xC0, next), do: parse_jpeg_dimensions(@ftype_base, next) 48 | defp parse_jpeg_sof(0xC2, next), do: parse_jpeg_dimensions(@ftype_prog, next) 49 | defp parse_jpeg_sof(_, next), do: parse_jpeg(next) 50 | 51 | defp parse_jpeg_dimensions( 52 | ftype, 53 | <<_skip::size(24), height::size(16), width::size(16), _::binary>> 54 | ) do 55 | {@mime, width, height, ftype} 56 | end 57 | 58 | defp parse_jpeg_dimensions(_, _), do: nil 59 | end 60 | -------------------------------------------------------------------------------- /lib/ex_image_info/types/png.ex: -------------------------------------------------------------------------------- 1 | defmodule ExImageInfo.Types.PNG do 2 | @moduledoc false 3 | 4 | @behaviour ExImageInfo.Detector 5 | 6 | @mime "image/png" 7 | @ftype "PNG" 8 | 9 | @signature <<"PNG\r\n", 0x1A, "\n">> 10 | @signature_ihdr <<"IHDR">> 11 | 12 | def seems?(<<_::bytes-size(1), @signature, _rest::binary>>), do: true 13 | def seems?(_), do: false 14 | 15 | def info( 16 | <<_::bytes-size(1), @signature, _::size(32), @signature_ihdr, width::size(32), 17 | height::size(32), _rest::binary>> 18 | ), 19 | do: {@mime, width, height, @ftype} 20 | 21 | def info(_), do: nil 22 | 23 | def type(<<_::size(8), @signature, _::size(32), @signature_ihdr, _rest::binary>>), 24 | do: {@mime, @ftype} 25 | 26 | def type(_), do: nil 27 | end 28 | -------------------------------------------------------------------------------- /lib/ex_image_info/types/pnm.ex: -------------------------------------------------------------------------------- 1 | defmodule ExImageInfo.Types.PNM do 2 | @moduledoc false 3 | 4 | @behaviour ExImageInfo.Detector 5 | # https://en.wikipedia.org/wiki/Netpbm_format 6 | 7 | # promote to 3 mime types? (x-portable-bitmap, x-portable-graymap, x-portable-pixmap) 8 | @mime "image/x-portable-anymap" 9 | 10 | @ftype_pbm "PNMpbm" 11 | @ftype_pgm "PNMpgm" 12 | @ftype_ppm "PNMppm" 13 | 14 | @signature <<"P">> 15 | 16 | @sharp 0x23 17 | @nl 0x0A 18 | @space 0x20 19 | 20 | def seems?(<<@signature, char::size(8), split, _rest::binary>>) 21 | when split in [@nl, @space, @sharp] do 22 | if signature_pnm(char), do: true, else: false 23 | end 24 | 25 | def seems?(_), do: false 26 | 27 | def info(<<@signature, char::size(8), split, rest::binary>>) 28 | when split in [@nl, @space, @sharp] do 29 | with type when not is_nil(type) <- signature_pnm(char), 30 | {w, h} <- parse(rest, nil) do 31 | {@mime, String.to_integer(w), String.to_integer(h), type} 32 | else 33 | _ -> nil 34 | end 35 | end 36 | 37 | def info(_), do: nil 38 | 39 | def type(<<@signature, char::size(8), split, _rest::binary>>) 40 | when split in [@nl, @space, @sharp] do 41 | case signature_pnm(char) do 42 | nil -> nil 43 | type -> {@mime, type} 44 | end 45 | end 46 | 47 | def type(_), do: nil 48 | 49 | defp signature_pnm(char) do 50 | case char do 51 | x when x in [?1, ?4] -> @ftype_pbm 52 | x when x in [?2, ?5] -> @ftype_pgm 53 | x when x in [?3, ?6] -> @ftype_ppm 54 | _ -> nil 55 | end 56 | end 57 | 58 | defp parse(rest, pre) do 59 | # no global 60 | case :binary.split(rest, "\n") do 61 | [line, next] -> 62 | # comments 63 | valid = hd(:binary.split(line, "#")) 64 | ret = Regex.run(~r/^\s*(\d+)(?:[\s]|)(?:(\d+)(?:[\s]|))?/, valid) 65 | 66 | case ret do 67 | [_, w, h] -> 68 | if pre == nil do 69 | {w, h} 70 | else 71 | {pre, w} 72 | end 73 | 74 | [_, w] -> 75 | # credo:disable-for-next-line Credo.Check.Refactor.Nesting 76 | if pre == nil do 77 | parse(next, w) 78 | else 79 | {pre, w} 80 | end 81 | 82 | _ -> 83 | parse(next, pre) 84 | end 85 | 86 | _ -> 87 | nil 88 | end 89 | end 90 | end 91 | -------------------------------------------------------------------------------- /lib/ex_image_info/types/psd.ex: -------------------------------------------------------------------------------- 1 | defmodule ExImageInfo.Types.PSD do 2 | @moduledoc false 3 | 4 | @behaviour ExImageInfo.Detector 5 | 6 | @mime "image/psd" 7 | @ftype "PSD" 8 | 9 | @signature <<"8BPS">> 10 | 11 | def seems?(<<@signature, _::binary>>), do: true 12 | def seems?(_), do: false 13 | 14 | def info( 15 | <<@signature, _skip::bytes-size(10), height::size(32), width::size(32), 16 | _::binary>> 17 | ), 18 | do: {@mime, width, height, @ftype} 19 | 20 | def info(_), do: nil 21 | 22 | def type(<<@signature, _::binary>>), do: {@mime, @ftype} 23 | def type(_), do: nil 24 | end 25 | -------------------------------------------------------------------------------- /lib/ex_image_info/types/tiff.ex: -------------------------------------------------------------------------------- 1 | defmodule ExImageInfo.Types.TIFF do 2 | @moduledoc false 3 | 4 | @behaviour ExImageInfo.Detector 5 | # https://en.wikipedia.org/wiki/TIFF 6 | 7 | require Bitwise 8 | 9 | @mime "image/tiff" 10 | @ftype_ii "TIFFII" 11 | @ftype_mm "TIFFMM" 12 | 13 | @signature_ii <<"II", 0x2A00::size(16)>> 14 | @signature_mm <<"MM", 0x002A::size(16)>> 15 | 16 | def seems?(<<@signature_ii, _rest::binary>>), do: true 17 | def seems?(<<@signature_mm, _rest::binary>>), do: true 18 | def seems?(_), do: false 19 | 20 | def info(<<@signature_ii, rest::binary>>), 21 | do: parse_tiff(false, rest, 4 + byte_size(rest)) 22 | 23 | def info(<<@signature_mm, rest::binary>>), 24 | do: parse_tiff(true, rest, 4 + byte_size(rest)) 25 | 26 | def info(_), do: nil 27 | 28 | def type(<<@signature_ii, _rest::binary>>), do: {@mime, @ftype_ii} 29 | def type(<<@signature_mm, _rest::binary>>), do: {@mime, @ftype_mm} 30 | def type(_), do: nil 31 | 32 | defp parse_tiff(false = b_e, <>, fsize) do 33 | parse_tiff_block(b_e, idf_off, rest, fsize) 34 | end 35 | 36 | defp parse_tiff(true = b_e, <>, fsize) do 37 | parse_tiff_block(b_e, idf_off, rest, fsize) 38 | end 39 | 40 | defp parse_tiff(_, _, _), do: nil 41 | 42 | defp parse_tiff_block(b_e, idf_off, rest, fsize) do 43 | buff_size = 1024 44 | 45 | buff_size = 46 | if idf_off + buff_size > fsize, do: fsize - idf_off - 10, else: buff_size 47 | 48 | # @signature_xx (4), idf_off::size(32) (4) 49 | idf_off_pos = idf_off - 4 - 4 50 | 51 | case rest do 52 | <<_skip1::bytes-size(idf_off_pos), _skip2::bytes-size(2), 53 | buff_idf::bytes-size(buff_size), _::binary>> -> 54 | tags = parse_tiff_tags(b_e, buff_idf, %{}) 55 | ftype = if b_e == false, do: @ftype_ii, else: @ftype_mm 56 | w = Map.get(tags, 256) 57 | h = Map.get(tags, 257) 58 | if w != nil and h != nil, do: {@mime, w, h, ftype} 59 | 60 | _ -> 61 | nil 62 | end 63 | end 64 | 65 | defp parse_tiff_nexttags(<<_skip::bytes-size(12), rest::binary>>) do 66 | if byte_size(rest) > 12, do: rest, else: <<>> 67 | end 68 | 69 | defp parse_tiff_nexttags(_rest), do: <<>> 70 | 71 | defp parse_tiff_tags(_b_e, <<>>, tags) do 72 | tags 73 | end 74 | 75 | defp parse_tiff_tags( 76 | true = b_e, 77 | <> = buff, 79 | tags 80 | ) do 81 | parse_tiff_tags(b_e, code, type, length, low, high, rest, buff, tags) 82 | end 83 | 84 | defp parse_tiff_tags( 85 | false = b_e, 86 | <> = buff, 88 | tags 89 | ) do 90 | parse_tiff_tags(b_e, code, type, length, low, high, rest, buff, tags) 91 | end 92 | 93 | defp parse_tiff_tags( 94 | b_e, 95 | code, 96 | type, 97 | length, 98 | low, 99 | high, 100 | <<_rest::binary>>, 101 | buff, 102 | tags 103 | ) do 104 | if code == 0 do 105 | tags 106 | else 107 | tags = 108 | if length == 1 and (type == 3 or type == 4) do 109 | val = Bitwise.<<<(high, 16) + low 110 | Map.put(tags, code, val) 111 | else 112 | tags 113 | end 114 | 115 | buff = parse_tiff_nexttags(buff) 116 | parse_tiff_tags(b_e, buff, tags) 117 | end 118 | end 119 | end 120 | -------------------------------------------------------------------------------- /lib/ex_image_info/types/webp.ex: -------------------------------------------------------------------------------- 1 | defmodule ExImageInfo.Types.WEBP do 2 | @moduledoc false 3 | 4 | @behaviour ExImageInfo.Detector 5 | 6 | require Bitwise 7 | 8 | @mime "image/webp" 9 | @ftype_vp8 "webpVP8" 10 | @ftype_vp8l "webpVP8L" 11 | @ftype_vp8x "webpVP8X" 12 | 13 | @signature_riff <<"RIFF">> 14 | @signature_webp <<"WEBP">> 15 | @signature_vp8 <<"VP8">> 16 | 17 | def seems?( 18 | <<@signature_riff, _skip::bytes-size(4), @signature_webp, @signature_vp8, 19 | _rest::binary>> 20 | ), 21 | do: true 22 | 23 | def seems?(_), do: false 24 | 25 | def info( 26 | <<@signature_riff, _skip::bytes-size(4), @signature_webp, @signature_vp8, 27 | lossy::bytes-size(1), _skip2::bytes-size(4), first::bytes-size(1), 28 | next::bytes-size(9), _rest::binary>> 29 | ) do 30 | <<_skip::bytes-size(2), sign::bytes-size(3), _::binary>> = next 31 | 32 | cond do 33 | lossy == " " and first != <<0x2F>> -> parse_lossy(next) 34 | lossy == "L" and sign != <<0x9D012A::size(24)>> -> parse_lossless(next) 35 | lossy == "X" -> parse_vp8xbitstream(next) 36 | true -> nil 37 | end 38 | end 39 | 40 | def info(_), do: nil 41 | 42 | def type( 43 | <<@signature_riff, _skip::bytes-size(4), @signature_webp, @signature_vp8, 44 | lossy::bytes-size(1), _rest::binary>> 45 | ) do 46 | cond do 47 | lossy == " " -> {@mime, @ftype_vp8} 48 | lossy == "L" -> {@mime, @ftype_vp8l} 49 | lossy == "X" -> {@mime, @ftype_vp8x} 50 | true -> nil 51 | end 52 | end 53 | 54 | def type(_), do: nil 55 | 56 | defp parse_lossy( 57 | <<_skip::bytes-size(5), w::little-size(16), h::little-size(16), _rest::binary>> 58 | ) do 59 | {@mime, Bitwise.band(w, 0x3FFF), Bitwise.band(h, 0x3FFF), @ftype_vp8} 60 | end 61 | 62 | defp parse_lossless( 63 | <> = 64 | _buf 65 | ) do 66 | w = 1 + Bitwise.bor(Bitwise.<<<(Bitwise.band(two, 0x3F), 8), one) 67 | 68 | h = 69 | 1 + 70 | Bitwise.bor( 71 | Bitwise.bor(Bitwise.<<<(Bitwise.band(four, 0xF), 10), Bitwise.<<<(three, 2)), 72 | Bitwise.>>>(Bitwise.band(two, 0xC0), 6) 73 | ) 74 | 75 | {@mime, w, h, @ftype_vp8l} 76 | end 77 | 78 | defp parse_vp8xbitstream( 79 | <<_skip::size(24), canvas_w::little-size(24), canvas_h::little-size(24), 80 | _rest::binary>> = _buf 81 | ) do 82 | {@mime, canvas_w + 1, canvas_h + 1, @ftype_vp8x} 83 | end 84 | 85 | defp parse_vp8xbitstream(_), do: nil 86 | end 87 | -------------------------------------------------------------------------------- /mix.exs: -------------------------------------------------------------------------------- 1 | defmodule ExImageInfo.Mixfile do 2 | use Mix.Project 3 | 4 | def project do 5 | [ 6 | app: :ex_image_info, 7 | description: 8 | "ExImageInfo is an Elixir library to parse images (binaries) and get the dimensions (size), detected mime-type and overall validity for a set of image formats. It is the fastest and supports multiple formats.", 9 | version: "VERSION" |> File.read!() |> String.trim(), 10 | elixir: "~> 1.13", 11 | name: "ExImageInfo", 12 | package: package(), 13 | build_embedded: Mix.env() == :prod, 14 | start_permanent: Mix.env() == :prod, 15 | aliases: aliases(), 16 | deps: deps(), 17 | docs: docs(), 18 | source_url: "https://github.com/Group4Layers/ex_image_info", 19 | homepage_url: "https://www.group4layers.com", 20 | elixirc_paths: elixirc_paths(Mix.env()), 21 | test_coverage: [tool: ExCoveralls], 22 | preferred_cli_env: [ 23 | coveralls: :test, 24 | "coveralls.detail": :test, 25 | "coveralls.html": :test, 26 | test_wip: :test, 27 | test_all: :test, 28 | coverage: :test, 29 | coverage_all: :test 30 | ] 31 | ] 32 | end 33 | 34 | defp elixirc_paths(:test), do: ["lib", "test", "test/fixtures/mocks/isobmff.exs"] 35 | 36 | defp elixirc_paths(_), do: ["lib"] 37 | 38 | def application do 39 | [] 40 | end 41 | 42 | defp deps do 43 | [ 44 | {:excoveralls, "~> 0.18", only: :test}, 45 | {:ex_doc, "~> 0.37", only: :dev}, 46 | {:inch_ex, "~> 2.0", only: [:dev, :test]}, 47 | {:credo, "~> 1.7", only: [:dev, :test], runtime: false}, 48 | {:styler, "~> 1.4", only: [:dev, :test], runtime: false} 49 | ] 50 | end 51 | 52 | defp aliases do 53 | [ 54 | test_wip: ["test --only wip"], 55 | test_all: ["test --include fetch_external"], 56 | coverage: ["coveralls.html"], 57 | coverage_all: ["coveralls.html test --include fetch_external"], 58 | test: ["test --exclude fetch_external"], 59 | lint: [ 60 | "format --check-formatted", 61 | "deps.unlock --check-unused", 62 | "credo --all --strict" 63 | ] 64 | ] 65 | end 66 | 67 | defp docs do 68 | [ 69 | extras: ["README.md", "LICENSE.md", "CHANGELOG.md", "CONTRIBUTORS.md"], 70 | assets: %{"assets/" => "assets"}, 71 | filter_modules: ~r/ExImageInfo$/ 72 | ] 73 | end 74 | 75 | defp package do 76 | [ 77 | name: :ex_image_info, 78 | # just the minimum for prod 79 | files: [ 80 | "lib", 81 | "README.md", 82 | "VERSION", 83 | "LICENSE.md", 84 | "mix.exs" 85 | ], 86 | maintainers: ["nozalr "], 87 | licenses: ["MIT"], 88 | links: %{ 89 | "GitHub" => "https://github.com/Group4Layers/ex_image_info", 90 | "Organization" => "https://www.group4layers.com" 91 | } 92 | ] 93 | end 94 | end 95 | -------------------------------------------------------------------------------- /mix.lock: -------------------------------------------------------------------------------- 1 | %{ 2 | "bunt": {:hex, :bunt, "0.2.1", "e2d4792f7bc0ced7583ab54922808919518d0e57ee162901a16a1b6664ef3b14", [:mix], [], "hexpm", "a330bfb4245239787b15005e66ae6845c9cd524a288f0d141c148b02603777a5"}, 3 | "credo": {:hex, :credo, "1.7.11", "d3e805f7ddf6c9c854fd36f089649d7cf6ba74c42bc3795d587814e3c9847102", [:mix], [{:bunt, "~> 0.2.1 or ~> 1.0", [hex: :bunt, repo: "hexpm", optional: false]}, {:file_system, "~> 0.2 or ~> 1.0", [hex: :file_system, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "56826b4306843253a66e47ae45e98e7d284ee1f95d53d1612bb483f88a8cf219"}, 4 | "earmark_parser": {:hex, :earmark_parser, "1.4.44", "f20830dd6b5c77afe2b063777ddbbff09f9759396500cdbe7523efd58d7a339c", [:mix], [], "hexpm", "4778ac752b4701a5599215f7030989c989ffdc4f6df457c5f36938cc2d2a2750"}, 5 | "ex_doc": {:hex, :ex_doc, "0.37.3", "f7816881a443cd77872b7d6118e8a55f547f49903aef8747dbcb345a75b462f9", [:mix], [{:earmark_parser, "~> 1.4.42", [hex: :earmark_parser, repo: "hexpm", optional: false]}, {:makeup_c, ">= 0.1.0", [hex: :makeup_c, repo: "hexpm", optional: true]}, {:makeup_elixir, "~> 0.14 or ~> 1.0", [hex: :makeup_elixir, repo: "hexpm", optional: false]}, {:makeup_erlang, "~> 0.1 or ~> 1.0", [hex: :makeup_erlang, repo: "hexpm", optional: false]}, {:makeup_html, ">= 0.1.0", [hex: :makeup_html, repo: "hexpm", optional: true]}], "hexpm", "e6aebca7156e7c29b5da4daa17f6361205b2ae5f26e5c7d8ca0d3f7e18972233"}, 6 | "excoveralls": {:hex, :excoveralls, "0.18.5", "e229d0a65982613332ec30f07940038fe451a2e5b29bce2a5022165f0c9b157e", [:mix], [{:castore, "~> 1.0", [hex: :castore, repo: "hexpm", optional: true]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "523fe8a15603f86d64852aab2abe8ddbd78e68579c8525ae765facc5eae01562"}, 7 | "file_system": {:hex, :file_system, "1.1.0", "08d232062284546c6c34426997dd7ef6ec9f8bbd090eb91780283c9016840e8f", [:mix], [], "hexpm", "bfcf81244f416871f2a2e15c1b515287faa5db9c6bcf290222206d120b3d43f6"}, 8 | "inch_ex": {:hex, :inch_ex, "2.0.0", "24268a9284a1751f2ceda569cd978e1fa394c977c45c331bb52a405de544f4de", [:mix], [{:bunt, "~> 0.2", [hex: :bunt, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "96d0ec5ecac8cf63142d02f16b7ab7152cf0f0f1a185a80161b758383c9399a8"}, 9 | "jason": {:hex, :jason, "1.4.4", "b9226785a9aa77b6857ca22832cffa5d5011a667207eb2a0ad56adb5db443b8a", [:mix], [{:decimal, "~> 1.0 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "c5eb0cab91f094599f94d55bc63409236a8ec69a21a67814529e8d5f6cc90b3b"}, 10 | "makeup": {:hex, :makeup, "1.2.1", "e90ac1c65589ef354378def3ba19d401e739ee7ee06fb47f94c687016e3713d1", [:mix], [{:nimble_parsec, "~> 1.4", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "d36484867b0bae0fea568d10131197a4c2e47056a6fbe84922bf6ba71c8d17ce"}, 11 | "makeup_elixir": {:hex, :makeup_elixir, "1.0.1", "e928a4f984e795e41e3abd27bfc09f51db16ab8ba1aebdba2b3a575437efafc2", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}, {:nimble_parsec, "~> 1.2.3 or ~> 1.3", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "7284900d412a3e5cfd97fdaed4f5ed389b8f2b4cb49efc0eb3bd10e2febf9507"}, 12 | "makeup_erlang": {:hex, :makeup_erlang, "1.0.2", "03e1804074b3aa64d5fad7aa64601ed0fb395337b982d9bcf04029d68d51b6a7", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}], "hexpm", "af33ff7ef368d5893e4a267933e7744e46ce3cf1f61e2dccf53a111ed3aa3727"}, 13 | "nimble_parsec": {:hex, :nimble_parsec, "1.4.2", "8efba0122db06df95bfaa78f791344a89352ba04baedd3849593bfce4d0dc1c6", [:mix], [], "hexpm", "4b21398942dda052b403bbe1da991ccd03a053668d147d53fb8c4e0efe09c973"}, 14 | "styler": {:hex, :styler, "1.4.1", "d3c4e90e000bec1cb61c49edea9c7c048bc1d89d5e3d88ef590313a3b793eea7", [:mix], [], "hexpm", "3539c44bc738f7da94462248f28009ca52a18a1b071fc07af11447ef5f13adbc"}, 15 | } 16 | -------------------------------------------------------------------------------- /test/ex_image_info_test.exs: -------------------------------------------------------------------------------- 1 | defmodule ExImageInfoTest do 2 | use ExUnit.Case 3 | 4 | doctest ExImageInfo 5 | end 6 | -------------------------------------------------------------------------------- /test/ex_image_info_test/images/bmp_test.exs: -------------------------------------------------------------------------------- 1 | defmodule ExImageInfoTest.Images.BMPTest do 2 | use ImageTestCase 3 | 4 | import ExImageInfo 5 | 6 | setup_all do 7 | images = %{ 8 | # Comments: GIMP options to export the original images 9 | "bmp" => read_image("valid/bmp/layers.bmp") 10 | } 11 | 12 | {:ok, images} 13 | end 14 | 15 | test "force - bmp disk image - #seems? #type #info", images do 16 | assert seems?(images["bmp"], :bmp) == true 17 | assert type(images["bmp"], :bmp) == {"image/bmp", "BMP"} 18 | assert info(images["bmp"], :bmp) == {"image/bmp", 130, 42, "BMP"} 19 | end 20 | 21 | test "guess - bmp disk image - #seems? #type #info", images do 22 | assert seems?(images["bmp"]) == :bmp 23 | assert type(images["bmp"]) == {"image/bmp", "BMP"} 24 | assert info(images["bmp"]) == {"image/bmp", 130, 42, "BMP"} 25 | end 26 | end 27 | -------------------------------------------------------------------------------- /test/ex_image_info_test/images/gif_test.exs: -------------------------------------------------------------------------------- 1 | defmodule ExImageInfoTest.Images.GIFTest do 2 | use ImageTestCase 3 | 4 | import ExImageInfo 5 | 6 | setup_all do 7 | images = %{ 8 | # Comments: GIMP options to export the original images 9 | # interlace 10 | "gif87a" => read_image("valid/gif/layers-87a.gif"), 11 | "gif89a" => read_image("valid/gif/layers.gif"), 12 | "gif-anim" => read_image("valid/gif/layers-anim.gif") 13 | } 14 | 15 | {:ok, images} 16 | end 17 | 18 | test "force - gif (gif87a, gif89a) disk image - #seems? #type #info", images do 19 | assert seems?(images["gif87a"], :gif) == true 20 | assert seems?(images["gif89a"], :gif) == true 21 | assert type(images["gif87a"], :gif) == {"image/gif", "GIF87a"} 22 | assert type(images["gif89a"], :gif) == {"image/gif", "GIF89a"} 23 | assert info(images["gif87a"], :gif) == {"image/gif", 130, 42, "GIF87a"} 24 | assert info(images["gif89a"], :gif) == {"image/gif", 130, 42, "GIF89a"} 25 | end 26 | 27 | test "guess - gif (gif87a, gif89a) disk image - #seems? #type #info", images do 28 | assert seems?(images["gif87a"]) == :gif 29 | assert seems?(images["gif89a"]) == :gif 30 | assert type(images["gif87a"]) == {"image/gif", "GIF87a"} 31 | assert type(images["gif89a"]) == {"image/gif", "GIF89a"} 32 | assert info(images["gif87a"]) == {"image/gif", 130, 42, "GIF87a"} 33 | assert info(images["gif89a"]) == {"image/gif", 130, 42, "GIF89a"} 34 | end 35 | 36 | test "gif (anim) disk image - #seems? #type #info", images do 37 | assert seems?(images["gif-anim"], :gif) == true 38 | assert type(images["gif-anim"], :gif) == {"image/gif", "GIF89a"} 39 | assert info(images["gif-anim"], :gif) == {"image/gif", 130, 42, "GIF89a"} 40 | end 41 | end 42 | -------------------------------------------------------------------------------- /test/ex_image_info_test/images/ico_test.exs: -------------------------------------------------------------------------------- 1 | defmodule ExImageInfoTest.Images.ICOTest do 2 | use ImageTestCase 3 | 4 | import ExImageInfo 5 | 6 | setup_all do 7 | images = %{ 8 | "ico" => read_image("valid/ico/wikipedia.ico"), 9 | "ico-256" => read_image("valid/ico/w.ico") 10 | } 11 | 12 | {:ok, images} 13 | end 14 | 15 | test "force - ico disk image - #seems? #type #info", images do 16 | assert seems?(images["ico"], :ico) == true 17 | assert seems?(images["ico-256"], :ico) == true 18 | assert type(images["ico"], :ico) == {"image/x-icon", "ICO"} 19 | assert type(images["ico-256"], :ico) == {"image/x-icon", "ICO"} 20 | assert info(images["ico"], :ico) == {"image/x-icon", 48, 48, "ICO"} 21 | assert info(images["ico-256"], :ico) == {"image/x-icon", 256, 256, "ICO"} 22 | end 23 | 24 | test "guess - ico disk image - #seems? #type #info", images do 25 | assert seems?(images["ico"]) == :ico 26 | assert seems?(images["ico-256"]) == :ico 27 | assert type(images["ico"]) == {"image/x-icon", "ICO"} 28 | assert type(images["ico-256"]) == {"image/x-icon", "ICO"} 29 | assert info(images["ico"]) == {"image/x-icon", 48, 48, "ICO"} 30 | assert info(images["ico-256"]) == {"image/x-icon", 256, 256, "ICO"} 31 | end 32 | end 33 | -------------------------------------------------------------------------------- /test/ex_image_info_test/images/isobmff_test.exs: -------------------------------------------------------------------------------- 1 | defmodule ExImageInfoTest.Images.ISOBMFFTest do 2 | @moduledoc "HEIF, HEIC and AVIF tests using image files" 3 | use ImageTestCase 4 | 5 | import ExImageInfo 6 | 7 | setup_all do 8 | images = %{ 9 | "heic" => read_image("valid/isobmff/layers.heic"), 10 | "heif" => read_image("valid/isobmff/layers.heif"), 11 | "heic-min" => read_image("valid/isobmff/layers-min.heic"), 12 | "heic-rotated" => read_image("valid/isobmff/layers-rotated.heic"), 13 | "avif" => read_image("valid/isobmff/layers.avif") 14 | } 15 | 16 | {:ok, images} 17 | end 18 | 19 | @tag :fetch_external 20 | @tag :tmp_dir 21 | test "guess - isobmff (heif/heic) external images fetched (sequences) - #seems? #type #info", 22 | %{tmp_dir: tmp_dir} do 23 | base_url = 24 | "https://github.com/sunsided/heic-conversion-service/raw/refs/heads/main/data/nokia/" 25 | 26 | image = "image_sequences/bird_burst.heic" 27 | image = fetch_binary(base_url <> image, tmp_dir) 28 | 29 | assert seems?(image) == :heif 30 | assert type(image) == {"image/heif-sequence", "HEIFS"} 31 | assert info(image) == {"image/heif-sequence", 1280, 720, "HEIFS"} 32 | 33 | image = "image_sequences/candle_animation.heic" 34 | image = fetch_binary(base_url <> image, tmp_dir) 35 | 36 | assert seems?(image) == :heif 37 | assert type(image) == {"image/heif-sequence", "HEIFS"} 38 | assert info(image) == {"image/heif-sequence", 256, 144, "HEIFS"} 39 | 40 | image = "image_sequences/rally_burst.heic" 41 | image = fetch_binary(base_url <> image, tmp_dir) 42 | 43 | assert seems?(image) == :heif 44 | assert type(image) == {"image/heif-sequence", "HEIFS"} 45 | assert info(image) == {"image/heif-sequence", 1280, 720, "HEIFS"} 46 | 47 | image = "image_sequences/sea1_animation.heic" 48 | image = fetch_binary(base_url <> image, tmp_dir) 49 | 50 | assert seems?(image) == :heif 51 | assert type(image) == {"image/heif-sequence", "HEIFS"} 52 | assert info(image) == {"image/heif-sequence", 256, 144, "HEIFS"} 53 | 54 | image = "image_sequences/starfield_animation.heic" 55 | image = fetch_binary(base_url <> image, tmp_dir) 56 | 57 | assert seems?(image) == :heif 58 | assert type(image) == {"image/heif-sequence", "HEIFS"} 59 | assert info(image) == {"image/heif-sequence", 256, 144, "HEIFS"} 60 | end 61 | 62 | @tag :fetch_external 63 | @tag :tmp_dir 64 | test "guess - isobmff (heif/heic) external images fetched - #seems? #type #info", %{ 65 | tmp_dir: tmp_dir 66 | } do 67 | base_url = 68 | "https://github.com/sdsykes/fastimage/raw/refs/heads/master/test/fixtures/" 69 | 70 | image = "heic/heic-single.heic" 71 | image = fetch_binary(base_url <> image, tmp_dir) 72 | 73 | assert seems?(image) == :heif 74 | assert type(image) == {"image/heif", "HEIF"} 75 | assert info(image) == {"image/heif", 1440, 960, "HEIF"} 76 | 77 | image = "heic/heic-empty.heic" 78 | image = fetch_binary(base_url <> image, tmp_dir) 79 | 80 | assert seems?(image) == :heic 81 | assert type(image) == {"image/heic", "HEIC"} 82 | assert info(image) == {"image/heic", 3992, 2992, "HEIC"} 83 | 84 | image = "heic/heic-iphone.heic" 85 | image = fetch_binary(base_url <> image, tmp_dir) 86 | 87 | # irot: 88 | assert seems?(image) == :heic 89 | assert type(image) == {"image/heic", "HEIC"} 90 | assert info(image) == {"image/heic", 4032, 3024, "HEIC"} 91 | 92 | image = "heic/heic-iphone7.heic" 93 | image = fetch_binary(base_url <> image, tmp_dir) 94 | 95 | # irot: 96 | assert seems?(image) == :heic 97 | assert type(image) == {"image/heic", "HEIC"} 98 | assert info(image) == {"image/heic", 4032, 3024, "HEIC"} 99 | 100 | image = "heic/test.heic" 101 | image = fetch_binary(base_url <> image, tmp_dir) 102 | 103 | assert seems?(image) == :heic 104 | assert type(image) == {"image/heic", "HEIC"} 105 | assert info(image) == {"image/heic", 700, 476, "HEIC"} 106 | 107 | image = "heic/test-meta-after-mdat.heic" 108 | image = fetch_binary(base_url <> image, tmp_dir) 109 | 110 | assert seems?(image) == :heic 111 | assert type(image) == {"image/heic", "HEIC"} 112 | assert info(image) == {"image/heic", 4000, 3000, "HEIC"} 113 | 114 | image = "heic/heic-maybebroken.HEIC" 115 | image = fetch_binary(base_url <> image, tmp_dir) 116 | 117 | assert seems?(image) == :heic 118 | assert type(image) == {"image/heic", "HEIC"} 119 | assert info(image) == {"image/heic", 4032, 3024, "HEIC"} 120 | 121 | image = "heic/heic-collection.heic" 122 | image = fetch_binary(base_url <> image, tmp_dir) 123 | 124 | assert seems?(image) == :heif 125 | assert type(image) == {"image/heif", "HEIF"} 126 | assert info(image) == {"image/heif", 1440, 960, "HEIF"} 127 | 128 | image = "heic/inverted.heic" 129 | image = fetch_binary(base_url <> image, tmp_dir) 130 | 131 | assert seems?(image) == :heic 132 | assert type(image) == {"image/heic", "HEIC"} 133 | assert info(image) == {"image/heic", 3024, 4032, "HEIC"} 134 | 135 | base_url = 136 | "https://github.com/sunsided/heic-conversion-service/raw/refs/heads/main/data/nokia/" 137 | 138 | image = "heifv2/stereo_1200x800.heic" 139 | image = fetch_binary(base_url <> image, tmp_dir) 140 | 141 | assert seems?(image) == :heif 142 | assert type(image) == {"image/heif", "HEIF"} 143 | assert info(image) == {"image/heif", 1200, 800, "HEIF"} 144 | end 145 | 146 | @tag :fetch_external 147 | @tag :tmp_dir 148 | test "guess - isobmff (avif) external images fetched - #seems? #type #info", %{ 149 | tmp_dir: tmp_dir 150 | } do 151 | base_url = 152 | "https://github.com/sdsykes/fastimage/raw/refs/heads/master/test/fixtures/" 153 | 154 | image = "avif/fox.avif" 155 | image = fetch_binary(base_url <> image, tmp_dir) 156 | 157 | assert seems?(image) == :avif 158 | assert type(image) == {"image/avif", "AVIF"} 159 | assert info(image) == {"image/avif", 1204, 799, "AVIF"} 160 | 161 | image = "avif/hato.avif" 162 | image = fetch_binary(base_url <> image, tmp_dir) 163 | 164 | assert seems?(image) == :avif 165 | assert type(image) == {"image/avif", "AVIF"} 166 | assert info(image) == {"image/avif", 3082, 2048, "AVIF"} 167 | 168 | image = "avif/kimono.avif" 169 | image = fetch_binary(base_url <> image, tmp_dir) 170 | 171 | assert seems?(image) == :avif 172 | assert type(image) == {"image/avif", "AVIF"} 173 | assert info(image) == {"image/avif", 722, 1024, "AVIF"} 174 | 175 | image = "avif/red_green_flash.avif" 176 | image = fetch_binary(base_url <> image, tmp_dir) 177 | 178 | assert seems?(image) == :avif 179 | assert type(image) == {"image/avif-sequence", "AVIFS"} 180 | assert info(image) == {"image/avif-sequence", 256, 256, "AVIFS"} 181 | 182 | image = "avif/star.avifs" 183 | image = fetch_binary(base_url <> image, tmp_dir) 184 | 185 | assert seems?(image) == :avif 186 | assert type(image) == {"image/avif-sequence", "AVIFS"} 187 | assert info(image) == {"image/avif-sequence", 159, 159, "AVIFS"} 188 | end 189 | 190 | test "force - isobmff disk image - #seems? #type #info", images do 191 | image = images["heic"] 192 | assert seems?(image, :heic) == true 193 | assert type(image, :heic) == {"image/heic", "HEIC"} 194 | assert info(image, :heic) == {"image/heic", 130, 64, "HEIC"} 195 | 196 | image = images["heic-min"] 197 | assert seems?(image, :heic) == true 198 | assert type(image, :heic) == {"image/heic", "HEIC"} 199 | assert info(image, :heic) == {"image/heic", 14, 4, "HEIC"} 200 | 201 | image = images["heif"] 202 | assert seems?(image, :heif) == true 203 | assert type(image, :heif) == {"image/heif", "HEIF"} 204 | assert info(image, :heif) == {"image/heif", 130, 42, "HEIF"} 205 | 206 | image = images["heic-rotated"] 207 | assert seems?(image, :heic) == true 208 | assert type(image, :heic) == {"image/heic", "HEIC"} 209 | assert info(image, :heic) == {"image/heic", 42, 130, "HEIC"} 210 | 211 | image = images["avif"] 212 | assert seems?(image, :avif) == true 213 | assert type(image, :avif) == {"image/avif", "AVIF"} 214 | assert info(image, :avif) == {"image/avif", 130, 42, "AVIF"} 215 | end 216 | 217 | test "guess - heic disk image - #seems? #type #info", images do 218 | image = images["heic"] 219 | assert seems?(image) == :heic 220 | assert type(image) == {"image/heic", "HEIC"} 221 | assert info(image) == {"image/heic", 130, 64, "HEIC"} 222 | 223 | image = images["heic-min"] 224 | assert seems?(image) == :heic 225 | assert type(image) == {"image/heic", "HEIC"} 226 | assert info(image) == {"image/heic", 14, 4, "HEIC"} 227 | 228 | image = images["heif"] 229 | assert seems?(image) == :heif 230 | assert type(image) == {"image/heif", "HEIF"} 231 | assert info(image) == {"image/heif", 130, 42, "HEIF"} 232 | 233 | image = images["heic-rotated"] 234 | assert seems?(image) == :heic 235 | assert type(image) == {"image/heic", "HEIC"} 236 | assert info(image) == {"image/heic", 42, 130, "HEIC"} 237 | 238 | image = images["avif"] 239 | assert seems?(image) == :avif 240 | assert type(image) == {"image/avif", "AVIF"} 241 | assert info(image) == {"image/avif", 130, 42, "AVIF"} 242 | end 243 | 244 | defp fetch_binary(url, tmp_dir, debug? \\ false) do 245 | true = Enum.all?([:inets, :ssl], &Application.ensure_started/1) 246 | 247 | filename = Path.basename(url) 248 | tmp_file = Path.join([tmp_dir, filename]) 249 | 250 | if debug?, do: IO.puts("Downloading #{url} to #{tmp_file}") 251 | 252 | case :httpc.request(:get, {~c(#{url}), []}, [timeout: 10_000], 253 | stream: ~c(#{tmp_file}) 254 | ) do 255 | {:ok, :saved_to_file} -> File.read!(tmp_file) 256 | {:error, reason} -> raise "Failed to download #{url}: #{inspect(reason)}" 257 | other -> raise "Failed to download #{url}: #{inspect(other)}" 258 | end 259 | end 260 | end 261 | -------------------------------------------------------------------------------- /test/ex_image_info_test/images/jp2_test.exs: -------------------------------------------------------------------------------- 1 | defmodule ExImageInfoTest.Images.JP2Test do 2 | use ImageTestCase 3 | 4 | import ExImageInfo 5 | 6 | setup_all do 7 | images = %{ 8 | "jp2" => read_image("valid/jp2/layers.jp2") 9 | } 10 | 11 | {:ok, images} 12 | end 13 | 14 | test "force - jp2 (jpeg 2000) disk image - #seems? #type #info", images do 15 | assert seems?(images["jp2"], :jp2) == true 16 | assert type(images["jp2"], :jp2) == {"image/jp2", "JP2"} 17 | assert info(images["jp2"], :jp2) == {"image/jp2", 130, 42, "JP2"} 18 | end 19 | 20 | test "guess - jp2 (jpeg 2000) disk image - #seems? #type #info", images do 21 | assert seems?(images["jp2"]) == :jp2 22 | assert type(images["jp2"]) == {"image/jp2", "JP2"} 23 | assert info(images["jp2"]) == {"image/jp2", 130, 42, "JP2"} 24 | end 25 | end 26 | -------------------------------------------------------------------------------- /test/ex_image_info_test/images/jpeg_test.exs: -------------------------------------------------------------------------------- 1 | defmodule ExImageInfoTest.Images.JPEGTest do 2 | use ImageTestCase 3 | 4 | import ExImageInfo 5 | 6 | setup_all do 7 | images = %{ 8 | # Comments: GIMP options to export the original images 9 | "jpegBase" => read_image("valid/jpeg/layers.jpeg"), 10 | # optimize, progressive 11 | "jpegProg" => read_image("valid/jpeg/layers-progressive.jpeg") 12 | } 13 | 14 | {:ok, images} 15 | end 16 | 17 | test "force - jpeg (baseline, progressive) disk image - #seems? #type #info", 18 | images do 19 | assert seems?(images["jpegBase"], :jpeg) == true 20 | assert seems?(images["jpegProg"], :jpeg) == true 21 | assert type(images["jpegBase"], :jpeg) == {"image/jpeg", "baseJPEG"} 22 | assert type(images["jpegProg"], :jpeg) == {"image/jpeg", "progJPEG"} 23 | assert info(images["jpegBase"], :jpeg) == {"image/jpeg", 130, 42, "baseJPEG"} 24 | assert info(images["jpegProg"], :jpeg) == {"image/jpeg", 130, 42, "progJPEG"} 25 | end 26 | 27 | test "guess - jpeg (baseline, progressive) disk image - #seems? #type #info", 28 | images do 29 | assert seems?(images["jpegBase"]) == :jpeg 30 | assert seems?(images["jpegProg"]) == :jpeg 31 | assert type(images["jpegBase"]) == {"image/jpeg", "baseJPEG"} 32 | assert type(images["jpegProg"]) == {"image/jpeg", "progJPEG"} 33 | assert info(images["jpegBase"]) == {"image/jpeg", 130, 42, "baseJPEG"} 34 | assert info(images["jpegProg"]) == {"image/jpeg", 130, 42, "progJPEG"} 35 | end 36 | 37 | test "force - alias jpg (baseline, progressive) disk image - #seems? #type #info", 38 | images do 39 | assert seems?(images["jpegBase"], :jpg) == true 40 | assert seems?(images["jpegProg"], :jpg) == true 41 | assert type(images["jpegBase"], :jpg) == {"image/jpeg", "baseJPEG"} 42 | assert type(images["jpegProg"], :jpg) == {"image/jpeg", "progJPEG"} 43 | assert info(images["jpegBase"], :jpg) == {"image/jpeg", 130, 42, "baseJPEG"} 44 | assert info(images["jpegProg"], :jpg) == {"image/jpeg", 130, 42, "progJPEG"} 45 | end 46 | end 47 | -------------------------------------------------------------------------------- /test/ex_image_info_test/images/png_test.exs: -------------------------------------------------------------------------------- 1 | defmodule ExImageInfoTest.Images.PNGTest do 2 | use ImageTestCase 3 | 4 | import ExImageInfo 5 | 6 | setup_all do 7 | images = %{ 8 | # Comments: GIMP options to export the original images 9 | "png" => read_image("valid/png/layers.png") 10 | } 11 | 12 | {:ok, images} 13 | end 14 | 15 | test "force - png disk image - #seems? #type #info", images do 16 | assert seems?(images["png"], :png) == true 17 | assert type(images["png"], :png) == {"image/png", "PNG"} 18 | assert info(images["png"], :png) == {"image/png", 130, 42, "PNG"} 19 | end 20 | 21 | test "guess - png disk image - #seems? #type #info", images do 22 | assert seems?(images["png"]) == :png 23 | assert type(images["png"]) == {"image/png", "PNG"} 24 | assert info(images["png"]) == {"image/png", 130, 42, "PNG"} 25 | end 26 | end 27 | -------------------------------------------------------------------------------- /test/ex_image_info_test/images/pnm_test.exs: -------------------------------------------------------------------------------- 1 | defmodule ExImageInfoTest.Images.PNMTest do 2 | use ImageTestCase 3 | 4 | import ExImageInfo 5 | 6 | setup_all do 7 | images = %{ 8 | "pbm" => read_image("valid/pnm/layers.pbm"), 9 | "pbm-plain" => read_image("valid/pnm/plain.pbm"), 10 | "pgm" => read_image("valid/pnm/layers.pgm"), 11 | "pgm-plain" => read_image("valid/pnm/plain.pgm"), 12 | "ppm" => read_image("valid/pnm/layers.ppm"), 13 | "ppm-plain" => read_image("valid/pnm/plain.ppm") 14 | } 15 | 16 | {:ok, images} 17 | end 18 | 19 | test "force - pnm (pbm - bitmap) disk image (+ plain) - #seems? #type #info", 20 | images do 21 | assert seems?(images["pbm"], :pnm) == true 22 | assert seems?(images["pbm-plain"], :pnm) == true 23 | assert type(images["pbm"], :pnm) == {"image/x-portable-anymap", "PNMpbm"} 24 | assert type(images["pbm-plain"], :pnm) == {"image/x-portable-anymap", "PNMpbm"} 25 | assert info(images["pbm"], :pnm) == {"image/x-portable-anymap", 130, 42, "PNMpbm"} 26 | 27 | assert info(images["pbm-plain"], :pnm) == 28 | {"image/x-portable-anymap", 3, 11, "PNMpbm"} 29 | end 30 | 31 | test "force - pnm (pgm - graymap) disk image (+ plain) - #seems? #type #info", 32 | images do 33 | assert seems?(images["pgm"], :pnm) == true 34 | assert seems?(images["pgm-plain"], :pnm) == true 35 | assert type(images["pgm"], :pnm) == {"image/x-portable-anymap", "PNMpgm"} 36 | assert type(images["pgm-plain"], :pnm) == {"image/x-portable-anymap", "PNMpgm"} 37 | assert info(images["pgm"], :pnm) == {"image/x-portable-anymap", 130, 42, "PNMpgm"} 38 | 39 | assert info(images["pgm-plain"], :pnm) == 40 | {"image/x-portable-anymap", 3, 11, "PNMpgm"} 41 | end 42 | 43 | test "force - pnm (ppm - pixmap) disk image (+ plain) - #seems? #type #info", 44 | images do 45 | assert seems?(images["ppm"], :pnm) == true 46 | assert seems?(images["ppm-plain"], :pnm) == true 47 | assert type(images["ppm"], :pnm) == {"image/x-portable-anymap", "PNMppm"} 48 | assert type(images["ppm-plain"], :pnm) == {"image/x-portable-anymap", "PNMppm"} 49 | assert info(images["ppm"], :pnm) == {"image/x-portable-anymap", 130, 42, "PNMppm"} 50 | 51 | assert info(images["ppm-plain"], :pnm) == 52 | {"image/x-portable-anymap", 2, 5, "PNMppm"} 53 | end 54 | 55 | test "guess - pnm (pbm - bitmap) disk image (+ plain) - #seems? #type #info", 56 | images do 57 | assert seems?(images["pbm"]) == :pnm 58 | assert seems?(images["pbm-plain"]) == :pnm 59 | assert type(images["pbm"]) == {"image/x-portable-anymap", "PNMpbm"} 60 | assert type(images["pbm-plain"]) == {"image/x-portable-anymap", "PNMpbm"} 61 | assert info(images["pbm"]) == {"image/x-portable-anymap", 130, 42, "PNMpbm"} 62 | assert info(images["pbm-plain"]) == {"image/x-portable-anymap", 3, 11, "PNMpbm"} 63 | end 64 | 65 | test "guess - pnm (pgm - graymap) disk image (+ plain) - #seems? #type #info", 66 | images do 67 | assert seems?(images["pgm"]) == :pnm 68 | assert seems?(images["pgm-plain"]) == :pnm 69 | assert type(images["pgm"]) == {"image/x-portable-anymap", "PNMpgm"} 70 | assert type(images["pgm-plain"]) == {"image/x-portable-anymap", "PNMpgm"} 71 | assert info(images["pgm"]) == {"image/x-portable-anymap", 130, 42, "PNMpgm"} 72 | assert info(images["pgm-plain"]) == {"image/x-portable-anymap", 3, 11, "PNMpgm"} 73 | end 74 | 75 | test "guess - pnm (ppm - pixmap) disk image (+ plain) - #seems? #type #info", 76 | images do 77 | assert seems?(images["ppm"]) == :pnm 78 | assert seems?(images["ppm-plain"]) == :pnm 79 | assert type(images["ppm"]) == {"image/x-portable-anymap", "PNMppm"} 80 | assert type(images["ppm-plain"]) == {"image/x-portable-anymap", "PNMppm"} 81 | assert info(images["ppm"]) == {"image/x-portable-anymap", 130, 42, "PNMppm"} 82 | assert info(images["ppm-plain"]) == {"image/x-portable-anymap", 2, 5, "PNMppm"} 83 | end 84 | end 85 | -------------------------------------------------------------------------------- /test/ex_image_info_test/images/psd_test.exs: -------------------------------------------------------------------------------- 1 | defmodule ExImageInfoTest.Images.PSDTest do 2 | use ImageTestCase 3 | 4 | import ExImageInfo 5 | 6 | setup_all do 7 | images = %{ 8 | # Comments: GIMP options to export the original images 9 | "psd" => read_image("valid/psd/layers.psd") 10 | } 11 | 12 | {:ok, images} 13 | end 14 | 15 | test "force - psd disk image - #seems? #type #info", images do 16 | assert seems?(images["psd"], :psd) == true 17 | assert type(images["psd"], :psd) == {"image/psd", "PSD"} 18 | assert info(images["psd"], :psd) == {"image/psd", 130, 42, "PSD"} 19 | end 20 | end 21 | -------------------------------------------------------------------------------- /test/ex_image_info_test/images/tiff_test.exs: -------------------------------------------------------------------------------- 1 | defmodule ExImageInfoTest.Images.TIFFTest do 2 | use ImageTestCase 3 | 4 | import ExImageInfo 5 | 6 | setup_all do 7 | images = %{ 8 | # Comments: GIMP options to export the original images 9 | "tiff" => read_image("valid/tiff/layers.tiff"), 10 | # i-s: image-size 11 | "i-s-big-endian" => read_image("valid/tiff/i-s-big-endian.tiff"), 12 | "tiff-deflate" => read_image("valid/tiff/layers-deflate.tiff"), 13 | "tiff-jpeg" => read_image("valid/tiff/layers-jpeg.tiff"), 14 | "tiff-lzw" => read_image("valid/tiff/layers-lzw.tiff"), 15 | "tiff-packbits" => read_image("valid/tiff/layers-packbits.tiff") 16 | } 17 | 18 | {:ok, images} 19 | end 20 | 21 | test "force - tiff disk image (no compression) - #seems? #type #info", images do 22 | assert seems?(images["tiff"], :tiff) == true 23 | assert type(images["tiff"], :tiff) == {"image/tiff", "TIFFII"} 24 | assert info(images["tiff"], :tiff) == {"image/tiff", 130, 42, "TIFFII"} 25 | end 26 | 27 | test "force - tiff disk image (big-endian) - #seems? #type #info", images do 28 | assert seems?(images["i-s-big-endian"], :tiff) == true 29 | assert type(images["i-s-big-endian"], :tiff) == {"image/tiff", "TIFFMM"} 30 | assert info(images["i-s-big-endian"], :tiff) == {"image/tiff", 123, 456, "TIFFMM"} 31 | end 32 | 33 | test "force - tiff disk image (deflate compression) - #seems? #type #info", images do 34 | assert seems?(images["tiff-deflate"], :tiff) == true 35 | assert type(images["tiff-deflate"], :tiff) == {"image/tiff", "TIFFII"} 36 | assert info(images["tiff-deflate"], :tiff) == {"image/tiff", 130, 42, "TIFFII"} 37 | end 38 | 39 | test "force - tiff disk image (jpeg compression) - #seems? #type #info", images do 40 | assert seems?(images["tiff-jpeg"], :tiff) == true 41 | assert type(images["tiff-jpeg"], :tiff) == {"image/tiff", "TIFFII"} 42 | assert info(images["tiff-jpeg"], :tiff) == {"image/tiff", 130, 42, "TIFFII"} 43 | end 44 | 45 | test "force - tiff disk image (lzw compression) - #seems? #type #info", images do 46 | assert seems?(images["tiff-lzw"], :tiff) == true 47 | assert type(images["tiff-lzw"], :tiff) == {"image/tiff", "TIFFII"} 48 | assert info(images["tiff-lzw"], :tiff) == {"image/tiff", 130, 42, "TIFFII"} 49 | end 50 | 51 | test "force - tiff disk image (packbits compression) - #seems? #type #info", images do 52 | assert seems?(images["tiff-packbits"], :tiff) == true 53 | assert type(images["tiff-packbits"], :tiff) == {"image/tiff", "TIFFII"} 54 | assert info(images["tiff-packbits"], :tiff) == {"image/tiff", 130, 42, "TIFFII"} 55 | end 56 | end 57 | -------------------------------------------------------------------------------- /test/ex_image_info_test/images/webp_test.exs: -------------------------------------------------------------------------------- 1 | defmodule ExImageInfoTest.Images.WEBPTest do 2 | use ImageTestCase 3 | 4 | import ExImageInfo 5 | 6 | setup_all do 7 | images = %{ 8 | # Comments: GIMP options to export the original images 9 | "webpVP8" => read_image("valid/webp/layers-lossy.webp"), 10 | "webpVP8L" => read_image("valid/webp/layers-lossless.webp"), 11 | "webpVP8X" => read_image("valid/webp/layers-anim.webp") 12 | } 13 | 14 | {:ok, images} 15 | end 16 | 17 | test "force - webp disk image (lossy) - #seems? #type #info", images do 18 | assert seems?(images["webpVP8"], :webp) == true 19 | assert type(images["webpVP8"], :webp) == {"image/webp", "webpVP8"} 20 | assert info(images["webpVP8"], :webp) == {"image/webp", 130, 42, "webpVP8"} 21 | end 22 | 23 | test "force - webp disk image (lossless) - #seems? #type #info", images do 24 | assert seems?(images["webpVP8L"], :webp) == true 25 | assert type(images["webpVP8L"], :webp) == {"image/webp", "webpVP8L"} 26 | assert info(images["webpVP8L"], :webp) == {"image/webp", 130, 42, "webpVP8L"} 27 | end 28 | 29 | test "force - webp disk image (anim) - #seems? #type #info", images do 30 | assert seems?(images["webpVP8X"], :webp) == true 31 | assert type(images["webpVP8X"], :webp) == {"image/webp", "webpVP8X"} 32 | assert info(images["webpVP8X"], :webp) == {"image/webp", 130, 42, "webpVP8X"} 33 | end 34 | end 35 | -------------------------------------------------------------------------------- /test/ex_image_info_test/mocks/bmp_test.exs: -------------------------------------------------------------------------------- 1 | defmodule ExImageInfoTest.Mocks.BMPTest do 2 | use ExUnit.Case, async: true 3 | 4 | import ExImageInfo 5 | 6 | setup_all do 7 | images = %{ 8 | "bmp" => << 9 | "BM", 10 | # 16 bytes offset 11 | 0::size(128), 12 | 134::little-size(16), 13 | # skip 14 | 0::size(16), 15 | 457::little-size(16) 16 | >> 17 | } 18 | 19 | {:ok, images} 20 | end 21 | 22 | test "force - bmp binary mock - #seems? #type #info", images do 23 | assert seems?(images["bmp"], :bmp) == true 24 | assert type(images["bmp"], :bmp) == {"image/bmp", "BMP"} 25 | assert info(images["bmp"], :bmp) == {"image/bmp", 134, 457, "BMP"} 26 | end 27 | 28 | test "guess - bmp binary mock - #seems? #type #info", images do 29 | assert seems?(images["bmp"]) == :bmp 30 | assert type(images["bmp"]) == {"image/bmp", "BMP"} 31 | assert info(images["bmp"]) == {"image/bmp", 134, 457, "BMP"} 32 | end 33 | end 34 | -------------------------------------------------------------------------------- /test/ex_image_info_test/mocks/gif_test.exs: -------------------------------------------------------------------------------- 1 | defmodule ExImageInfoTest.Mocks.GIFTest do 2 | use ExUnit.Case, async: true 3 | 4 | import ExImageInfo 5 | 6 | setup_all do 7 | images = %{ 8 | "gif89a" => 9 | <<0x47, 0x49, 0x46, 0x38, 0x39, 0x61, 123::little-size(16), 10 | 456::little-size(16)>>, 11 | "gif87a" => 12 | <<0x47, 0x49, 0x46, 0x38, 0x37, 0x61, 134::little-size(16), 13 | 457::little-size(16)>> 14 | } 15 | 16 | {:ok, images} 17 | end 18 | 19 | test "force - gif (gif87a, gif89a) binary mock - #seems? #type #info", images do 20 | assert seems?(images["gif87a"], :gif) == true 21 | assert seems?(images["gif89a"], :gif) == true 22 | assert type(images["gif87a"], :gif) == {"image/gif", "GIF87a"} 23 | assert type(images["gif89a"], :gif) == {"image/gif", "GIF89a"} 24 | assert info(images["gif87a"], :gif) == {"image/gif", 134, 457, "GIF87a"} 25 | assert info(images["gif89a"], :gif) == {"image/gif", 123, 456, "GIF89a"} 26 | end 27 | 28 | test "guess - gif (gif87a, gif89a) binary mock - #seems? #type #info", images do 29 | # IO.inspect images["gif89a"], base: :hex 30 | assert seems?(images["gif87a"]) == :gif 31 | assert seems?(images["gif89a"]) == :gif 32 | assert type(images["gif87a"]) == {"image/gif", "GIF87a"} 33 | assert type(images["gif89a"]) == {"image/gif", "GIF89a"} 34 | assert info(images["gif87a"]) == {"image/gif", 134, 457, "GIF87a"} 35 | assert info(images["gif89a"]) == {"image/gif", 123, 456, "GIF89a"} 36 | end 37 | end 38 | -------------------------------------------------------------------------------- /test/ex_image_info_test/mocks/ico_test.exs: -------------------------------------------------------------------------------- 1 | defmodule ExImageInfoTest.Mocks.ICOTest do 2 | use ExUnit.Case, async: true 3 | 4 | import ExImageInfo 5 | # doctest ExImageInfo.Detector 6 | 7 | setup_all do 8 | images = %{ 9 | "ico" => << 10 | 0x00, 11 | 0x00, 12 | # .ICO = 1 13 | 0x01, 14 | 0x00, 15 | # 1 image 16 | 0x01, 17 | 0x00, 18 | # first image 19 | # width 0 => 256 20 | 0x86, 21 | # height 0 => 256 22 | 0x39, 23 | # rest 24 | # 14 bytes 25 | 0::size(112), 26 | 0x00 27 | >>, 28 | "ico-256" => << 29 | 0x00, 30 | 0x00, 31 | # .ICO = 1 32 | 0x01, 33 | 0x00, 34 | # 2 images, but only "parsed" the first one 35 | 0x02, 36 | 0x00, 37 | # first image 38 | # width 39 | 0x86, 40 | # height 41 | 0x39, 42 | # 14 bytes 43 | 0::size(112), 44 | # second image 45 | # width 0 => 256 46 | 0x0, 47 | # height 0 => 256 48 | 0x0, 49 | # 14 bytes 50 | 0::size(112), 51 | # rest 52 | 0x00 53 | >> 54 | } 55 | 56 | {:ok, images} 57 | end 58 | 59 | test "force - ico binary mock - #seems? #type #info", images do 60 | assert seems?(images["ico"], :ico) == true 61 | assert seems?(images["ico-256"], :ico) == true 62 | assert type(images["ico"], :ico) == {"image/x-icon", "ICO"} 63 | assert type(images["ico-256"], :ico) == {"image/x-icon", "ICO"} 64 | assert info(images["ico"], :ico) == {"image/x-icon", 134, 57, "ICO"} 65 | assert info(images["ico-256"], :ico) == {"image/x-icon", 256, 256, "ICO"} 66 | end 67 | 68 | test "guess - ico binary mock - #seems? #type #info", images do 69 | assert seems?(images["ico"]) == :ico 70 | assert seems?(images["ico-256"]) == :ico 71 | assert type(images["ico"]) == {"image/x-icon", "ICO"} 72 | assert type(images["ico-256"]) == {"image/x-icon", "ICO"} 73 | assert info(images["ico"]) == {"image/x-icon", 134, 57, "ICO"} 74 | # It picks the largest image in the .ICO (in this mock, the second) 75 | assert info(images["ico-256"]) == {"image/x-icon", 256, 256, "ICO"} 76 | end 77 | end 78 | -------------------------------------------------------------------------------- /test/ex_image_info_test/mocks/isobmff_test.exs: -------------------------------------------------------------------------------- 1 | defmodule ExImageInfoTest.Mocks.ISOBMFFTest do 2 | @moduledoc "HEIF, HEIC and AVIF tests using binary mocks." 3 | use ImageTestCase 4 | 5 | import ExImageInfo 6 | 7 | alias ExImageInfoTest.Fixtures.Mocks.ISOBMFF, as: Fixtures 8 | 9 | setup_all do 10 | {:ok, Fixtures.images()} 11 | end 12 | 13 | test "force - heif/heic binary mock - #seems? #type #info (unimplemented regions)", 14 | images do 15 | # hdlr box w/o pict 16 | image = images["heic-hdlr-1"] 17 | assert seems?(image, :heic) == true 18 | assert type(image, :heic) == {"image/heic", "HEIC"} 19 | assert info(image, :heic) == nil 20 | 21 | # hdlr box size < 12 22 | image = images["heic-hdlr-2"] 23 | assert seems?(image, :heic) == true 24 | assert type(image, :heic) == {"image/heic", "HEIC"} 25 | assert info(image, :heic) == nil 26 | 27 | # ispe box w/o width and height 28 | image = images["heic-ispe"] 29 | assert seems?(image, :heic) == true 30 | assert type(image, :heic) == {"image/heic", "HEIC"} 31 | assert info(image, :heic) == nil 32 | 33 | # unknown box type 34 | image = images["heic-jxlc"] 35 | assert seems?(image, :heic) == true 36 | assert type(image, :heic) == {"image/heic", "HEIC"} 37 | assert info(image, :heic) == nil 38 | 39 | # meta size < 4 40 | image = images["heic-meta"] 41 | assert seems?(image, :heic) == true 42 | assert type(image, :heic) == {"image/heic", "HEIC"} 43 | assert info(image, :heic) == nil 44 | end 45 | 46 | test "force - heif/heic binary mock - #seems? #type #info (edge cases)", images do 47 | # not matching primary box + extended size 48 | image = images["heic-extended-size-and-wrong-primary-box"] 49 | assert seems?(image, :heic) == true 50 | assert type(image, :heic) == {"image/heic", "HEIC"} 51 | assert info(image, :heic) == nil 52 | end 53 | 54 | test "force - heif/heic/avif binary mock - #seems? #type #info", images do 55 | image = images["heic"] 56 | assert seems?(image, :heic) == true 57 | assert type(image, :heic) == {"image/heic", "HEIC"} 58 | assert info(image, :heic) == {"image/heic", 14, 4, "HEIC"} 59 | 60 | image = images["heif"] 61 | assert seems?(image, :heif) == true 62 | assert type(image, :heif) == {"image/heif", "HEIF"} 63 | assert info(image, :heif) == {"image/heif", 13, 4, "HEIF"} 64 | 65 | image = images["avif"] 66 | assert seems?(image, :avif) == true 67 | assert type(image, :avif) == {"image/avif", "AVIF"} 68 | assert info(image, :avif) == {"image/avif", 130, 42, "AVIF"} 69 | end 70 | 71 | test "guess - heif/heic/avif binary mock - #seems? #type #info", images do 72 | image = images["heic"] 73 | assert seems?(image) == :heic 74 | assert type(image) == {"image/heic", "HEIC"} 75 | assert info(image) == {"image/heic", 14, 4, "HEIC"} 76 | 77 | image = images["heif"] 78 | assert seems?(image) == :heif 79 | assert type(image) == {"image/heif", "HEIF"} 80 | assert info(image) == {"image/heif", 13, 4, "HEIF"} 81 | 82 | image = images["avif"] 83 | assert seems?(image) == :avif 84 | assert type(image) == {"image/avif", "AVIF"} 85 | assert info(image) == {"image/avif", 130, 42, "AVIF"} 86 | end 87 | 88 | malformed_cases = [ 89 | read_box_header: 270, 90 | ftyp: 12, 91 | hdlr: 60, 92 | ispe: 256, 93 | pitm: 114, 94 | skippable: 77 95 | ] 96 | 97 | for {location, len} <- malformed_cases do 98 | test "force - heif/heic binary mock - #seems? #type #info - malformed binary in #{location} when truncating 0..#{len}", 99 | context do 100 | %{"heic" => image, :test => test_name} = context 101 | 102 | {num, ""} = 103 | test_name 104 | |> Atom.to_string() 105 | |> String.split("..", trim: true) 106 | |> Enum.map(&String.trim(&1)) 107 | |> List.last() 108 | |> Integer.parse() 109 | 110 | image_truncated = binary_part(image, 0, num) 111 | assert seems?(image_truncated) == :heic 112 | assert type(image_truncated) == {"image/heic", "HEIC"} 113 | assert info(image_truncated, :heic) == nil 114 | end 115 | 116 | test "guess - heif/heic binary mock - #seems? #type #info - malformed binary in #{location} when truncating 0..#{len}", 117 | context do 118 | %{"heic" => image, :test => test_name} = context 119 | 120 | {num, ""} = 121 | test_name 122 | |> Atom.to_string() 123 | |> String.split("..", trim: true) 124 | |> Enum.map(&String.trim(&1)) 125 | |> List.last() 126 | |> Integer.parse() 127 | 128 | image_truncated = binary_part(image, 0, num) 129 | assert seems?(image_truncated) == :heic 130 | assert type(image_truncated) == {"image/heic", "HEIC"} 131 | assert info(image_truncated) == nil 132 | end 133 | end 134 | end 135 | -------------------------------------------------------------------------------- /test/ex_image_info_test/mocks/jp2_test.exs: -------------------------------------------------------------------------------- 1 | defmodule ExImageInfoTest.Mocks.JP2Test do 2 | use ExUnit.Case, async: true 3 | 4 | import ExImageInfo 5 | 6 | setup_all do 7 | images = %{ 8 | "jp2" => << 9 | 0x00, 10 | 0x00, 11 | 0x00, 12 | 0x0C, 13 | 0x6A, 14 | 0x50, 15 | 0x20, 16 | 0x20, 17 | 0x0D, 18 | 0x0A, 19 | 0x87, 20 | 0x0A, 21 | 0x00, 22 | 0x00, 23 | 0x00, 24 | 0x14, 25 | 0x66, 26 | 0x74, 27 | 0x79, 28 | 0x70, 29 | 0x6A, 30 | 0x70, 31 | 0x32, 32 | 0x00, 33 | # 20 * 8 34 | 0::size(160), 35 | "ihdr", 36 | # height 37 | 457::size(32), 38 | # width 39 | 134::size(32) 40 | >> 41 | } 42 | 43 | {:ok, images} 44 | end 45 | 46 | test "force - jp2 (jpeg 2000) binary mock - #seems? #type #info", images do 47 | assert seems?(images["jp2"], :jp2) == true 48 | assert type(images["jp2"], :jp2) == {"image/jp2", "JP2"} 49 | assert info(images["jp2"], :jp2) == {"image/jp2", 134, 457, "JP2"} 50 | end 51 | 52 | test "guess - jp2 (jpeg 2000) binary mock - #seems? #type #info", images do 53 | assert seems?(images["jp2"]) == :jp2 54 | assert type(images["jp2"]) == {"image/jp2", "JP2"} 55 | assert info(images["jp2"]) == {"image/jp2", 134, 457, "JP2"} 56 | end 57 | end 58 | -------------------------------------------------------------------------------- /test/ex_image_info_test/mocks/jpeg_test.exs: -------------------------------------------------------------------------------- 1 | defmodule ExImageInfoTest.Mocks.JPEGTest do 2 | use ExUnit.Case, async: true 3 | 4 | import ExImageInfo 5 | 6 | setup_all do 7 | images = %{ 8 | "jpegBase" => << 9 | 0xFF, 10 | 0xD8, 11 | # skip 12 | 0xFF, 13 | 0xE8, 14 | # block length 15 | 0x00, 16 | 0x03, 17 | # content (as set in block length - 2) 18 | 0x00, 19 | # static 20 | 0xFF, 21 | # 0xC0, 0xC2 22 | 0xC0, 23 | # _skip 24 | 0x00, 25 | 0x00, 26 | 0x00, 27 | # height 28 | 457::size(16), 29 | # width 30 | 134::size(16) 31 | >>, 32 | "jpegProg" => << 33 | 0xFF, 34 | 0xD8, 35 | # skip 36 | 0xFF, 37 | 0xC2, 38 | # block length 39 | 0x00, 40 | 0x03, 41 | # content (as set in block length - 2 - the size of the length itself) 42 | 0x00, 43 | # static 44 | 0xFF, 45 | # 0xC0, 0xC2 46 | 0xC2, 47 | # _skip 48 | 0x00, 49 | 0x00, 50 | 0x00, 51 | # height 52 | 457::size(16), 53 | # width 54 | 134::size(16) 55 | # 0x00, 0x05, # height 56 | # 0x00, 0x08, # width 57 | >> 58 | } 59 | 60 | {:ok, images} 61 | end 62 | 63 | test "force - jpeg (baseline, progressive) binary mock - #seems? #type #info", 64 | images do 65 | assert seems?(images["jpegBase"], :jpeg) == true 66 | assert seems?(images["jpegProg"], :jpeg) == true 67 | assert type(images["jpegBase"], :jpeg) == {"image/jpeg", "baseJPEG"} 68 | assert type(images["jpegProg"], :jpeg) == {"image/jpeg", "progJPEG"} 69 | assert info(images["jpegBase"], :jpeg) == {"image/jpeg", 134, 457, "baseJPEG"} 70 | assert info(images["jpegProg"], :jpeg) == {"image/jpeg", 134, 457, "progJPEG"} 71 | end 72 | 73 | test "guess - jpeg (baseline, progressive) binary mock - #seems? #type #info", 74 | images do 75 | assert seems?(images["jpegBase"]) == :jpeg 76 | assert seems?(images["jpegProg"]) == :jpeg 77 | assert type(images["jpegBase"]) == {"image/jpeg", "baseJPEG"} 78 | assert type(images["jpegProg"]) == {"image/jpeg", "progJPEG"} 79 | assert info(images["jpegBase"]) == {"image/jpeg", 134, 457, "baseJPEG"} 80 | assert info(images["jpegProg"]) == {"image/jpeg", 134, 457, "progJPEG"} 81 | end 82 | end 83 | -------------------------------------------------------------------------------- /test/ex_image_info_test/mocks/png_test.exs: -------------------------------------------------------------------------------- 1 | defmodule ExImageInfoTest.Mocks.PNGTest do 2 | use ExUnit.Case, async: true 3 | 4 | import ExImageInfo 5 | 6 | setup_all do 7 | images = %{ 8 | "png" => << 9 | 0x89, 10 | 0x50, 11 | 0x4E, 12 | 0x47, 13 | 0x0D, 14 | 0x0A, 15 | 0x1A, 16 | 0x0A, 17 | # 0x00, 0x00, 0x00, 0x00, 18 | 0::size(32), 19 | "IHDR", 20 | # 0x00, 0x00, 0x00, 0x00, 21 | # 0x00_00_00_00::size(32), 22 | # 0::size(32), 23 | 134::size(32), 24 | 457::size(32), 25 | 0 26 | >> 27 | } 28 | 29 | {:ok, images} 30 | end 31 | 32 | test "force - png binary mock - #seems? #type #info", images do 33 | assert seems?(images["png"], :png) == true 34 | assert type(images["png"], :png) == {"image/png", "PNG"} 35 | assert info(images["png"], :png) == {"image/png", 134, 457, "PNG"} 36 | end 37 | 38 | test "guess - png binary mock - #seems? #type #info", images do 39 | assert seems?(images["png"]) == :png 40 | assert type(images["png"]) == {"image/png", "PNG"} 41 | assert info(images["png"]) == {"image/png", 134, 457, "PNG"} 42 | end 43 | end 44 | -------------------------------------------------------------------------------- /test/ex_image_info_test/mocks/pnm_test.exs: -------------------------------------------------------------------------------- 1 | defmodule ExImageInfoTest.Mocks.PNMTest do 2 | use ExUnit.Case, async: true 3 | 4 | import ExImageInfo 5 | 6 | setup_all do 7 | images = %{ 8 | "pbm" => << 9 | "P4", 10 | 0x0A, 11 | # width 12 | "134", 13 | 0x20, 14 | # height 15 | "457", 16 | 0x0A, 17 | 0x00 18 | >>, 19 | "pgm" => << 20 | "P5", 21 | 0x0A, 22 | # width 23 | "134", 24 | 0x20, 25 | # height 26 | "457", 27 | 0x0A, 28 | 0x00 29 | >>, 30 | "ppm" => << 31 | "P6", 32 | 0x0A, 33 | # width 34 | "134", 35 | 0x20, 36 | # height 37 | "457", 38 | 0x0A, 39 | 0x00 40 | >>, 41 | "pbm-plain" => """ 42 | P1 # comment 43 | 134 457 44 | 0 1 0 1 0 0 1 45 | 0 1 1 1 1 0 0 46 | """, 47 | "pgm-plain" => """ 48 | P2 49 | # comments 50 | # more comments + space 51 | 134 # alone 52 | 457 # other line + space 53 | 5 54 | 0 2 3 4 0 5 1 55 | 0 2 3 0 0 1 2 56 | """, 57 | "ppm-plain" => """ 58 | P3 59 | 134# comments 60 | # more comments 61 | 457 5 0 2 3 62 | """, 63 | "ppm-wrong" => """ 64 | P 65 | 134# comments 66 | # more comments 67 | 457 5 0 2 3 68 | """ 69 | } 70 | 71 | {:ok, images} 72 | end 73 | 74 | test "force - pnm (pbm - bitmap) binary and plain mock - #seems? #type #info", 75 | images do 76 | assert seems?(images["pbm"], :pnm) == true 77 | assert seems?(images["pbm-plain"], :pnm) == true 78 | assert type(images["pbm"], :pnm) == {"image/x-portable-anymap", "PNMpbm"} 79 | assert type(images["pbm-plain"], :pnm) == {"image/x-portable-anymap", "PNMpbm"} 80 | assert info(images["pbm"], :pnm) == {"image/x-portable-anymap", 134, 457, "PNMpbm"} 81 | 82 | assert info(images["pbm-plain"], :pnm) == 83 | {"image/x-portable-anymap", 134, 457, "PNMpbm"} 84 | end 85 | 86 | test "force - pnm (pgm - graymap) binary and plain mock - #seems? #type #info", 87 | images do 88 | assert seems?(images["pgm"], :pnm) == true 89 | assert seems?(images["pgm-plain"], :pnm) == true 90 | assert type(images["pgm"], :pnm) == {"image/x-portable-anymap", "PNMpgm"} 91 | assert type(images["pgm-plain"], :pnm) == {"image/x-portable-anymap", "PNMpgm"} 92 | assert info(images["pgm"], :pnm) == {"image/x-portable-anymap", 134, 457, "PNMpgm"} 93 | 94 | assert info(images["pgm-plain"], :pnm) == 95 | {"image/x-portable-anymap", 134, 457, "PNMpgm"} 96 | end 97 | 98 | test "force - pnm (ppm - pixmap) binary and plain mock - #seems? #type #info", 99 | images do 100 | assert seems?(images["ppm"], :pnm) == true 101 | assert seems?(images["ppm-plain"], :pnm) == true 102 | assert type(images["ppm"], :pnm) == {"image/x-portable-anymap", "PNMppm"} 103 | assert type(images["ppm-plain"], :pnm) == {"image/x-portable-anymap", "PNMppm"} 104 | assert info(images["ppm"], :pnm) == {"image/x-portable-anymap", 134, 457, "PNMppm"} 105 | 106 | assert info(images["ppm-plain"], :pnm) == 107 | {"image/x-portable-anymap", 134, 457, "PNMppm"} 108 | end 109 | 110 | test "guess - pnm (pbm - bitmap) binary and plain mock - #seems? #type #info", 111 | images do 112 | assert seems?(images["pbm"]) == :pnm 113 | assert seems?(images["pbm-plain"]) == :pnm 114 | assert type(images["pbm"]) == {"image/x-portable-anymap", "PNMpbm"} 115 | assert type(images["pbm-plain"]) == {"image/x-portable-anymap", "PNMpbm"} 116 | assert info(images["pbm"]) == {"image/x-portable-anymap", 134, 457, "PNMpbm"} 117 | assert info(images["pbm-plain"]) == {"image/x-portable-anymap", 134, 457, "PNMpbm"} 118 | end 119 | 120 | test "guess - pnm (pgm - graymap) binary and plain mock - #seems? #type #info", 121 | images do 122 | assert seems?(images["pgm"]) == :pnm 123 | assert seems?(images["pgm-plain"]) == :pnm 124 | assert type(images["pgm"]) == {"image/x-portable-anymap", "PNMpgm"} 125 | assert type(images["pgm-plain"]) == {"image/x-portable-anymap", "PNMpgm"} 126 | assert info(images["pgm"]) == {"image/x-portable-anymap", 134, 457, "PNMpgm"} 127 | assert info(images["pgm-plain"]) == {"image/x-portable-anymap", 134, 457, "PNMpgm"} 128 | end 129 | 130 | test "guess - pnm (ppm - pixmap) binary and plain mock - #seems? #type #info", 131 | images do 132 | assert seems?(images["ppm"]) == :pnm 133 | assert seems?(images["ppm-plain"]) == :pnm 134 | assert type(images["ppm"]) == {"image/x-portable-anymap", "PNMppm"} 135 | assert type(images["ppm-plain"]) == {"image/x-portable-anymap", "PNMppm"} 136 | assert info(images["ppm"]) == {"image/x-portable-anymap", 134, 457, "PNMppm"} 137 | assert info(images["ppm-plain"]) == {"image/x-portable-anymap", 134, 457, "PNMppm"} 138 | end 139 | 140 | test "ppm wrong plain mock (force and guess) - #seems? #type #info", 141 | images do 142 | assert seems?(images["ppm-wrong"], :pnm) == false 143 | assert type(images["ppm-wrong"], :pnm) == nil 144 | assert info(images["ppm-wrong"], :pnm) == nil 145 | 146 | assert seems?(images["ppm-wrong"]) == nil 147 | assert type(images["ppm-wrong"]) == nil 148 | assert info(images["ppm-wrong"]) == nil 149 | end 150 | end 151 | -------------------------------------------------------------------------------- /test/ex_image_info_test/mocks/psd_test.exs: -------------------------------------------------------------------------------- 1 | defmodule ExImageInfoTest.Mocks.PSDTest do 2 | use ExUnit.Case, async: true 3 | 4 | import ExImageInfo 5 | 6 | setup_all do 7 | images = %{ 8 | "psd" => << 9 | "8BPS", 10 | # 14 - 4 = 10 * 8 11 | 0::size(80), 12 | 457::size(32), 13 | 134::size(32) 14 | >> 15 | } 16 | 17 | {:ok, images} 18 | end 19 | 20 | test "force - psd binary mock - #seems? #type #info", images do 21 | assert seems?(images["psd"], :psd) == true 22 | assert type(images["psd"], :psd) == {"image/psd", "PSD"} 23 | assert info(images["psd"], :psd) == {"image/psd", 134, 457, "PSD"} 24 | end 25 | end 26 | -------------------------------------------------------------------------------- /test/ex_image_info_test/mocks/tiff_test.exs: -------------------------------------------------------------------------------- 1 | defmodule ExImageInfoTest.Mocks.TIFFTest do 2 | use ExUnit.Case, async: true 3 | 4 | import ExImageInfo 5 | 6 | setup_all do 7 | images = %{ 8 | # little endian 9 | "tiffII" => 10 | <<0x49492A00::size(32), 0x0C000000::size(32), 0xFFFFFF001000FE00::size(64), 11 | 0x04000100000000000000000103000100::size(128), 12 | 0x00000100000001010300010000000100::size(128), 13 | 0x00000201030003000000E20000000301::size(128), 14 | 0x03000100000001000000060103000100::size(128), 15 | 0x0000020000000D01020055000000E800::size(128), 16 | 0x00001101040001000000080000001201::size(128), 17 | 0x03000100000001000000150103000100::size(128), 18 | 0x00000300000016010300010000004000::size(128), 19 | 0x00001701040001000000030000001A01::size(128), 20 | 0x050001000000D20000001B0105000100::size(128), 21 | 0x0000DA0000001C010300010000000100::size(128), 22 | 0x00002801030001000000020000000000::size(128), 23 | 0x00006000000001000000600000000100::size(128), 24 | 0x00000800080008002F6D6E742F646174::size(128), 25 | 0x612F7265706F7369746F726965732F65::size(128), 26 | 0x6C697869722F65785F696D6167655F73::size(128), 27 | 0x697A652F746573742F66697874757265::size(128), 28 | 0x732F696D616765732F76616C69642F74::size(128), 29 | 0x6966662F6F6E652E7469666600::size(104)>>, 30 | # big endian 31 | "tiffMM" => <<0x4D4D002A::size(32), 0x00>> 32 | } 33 | 34 | {:ok, images} 35 | end 36 | 37 | test "force - tiff binary mock - #seems? #type #info", images do 38 | assert seems?(images["tiffII"], :tiff) == true 39 | assert seems?(images["tiffMM"], :tiff) == true 40 | assert type(images["tiffII"], :tiff) == {"image/tiff", "TIFFII"} 41 | assert type(images["tiffMM"], :tiff) == {"image/tiff", "TIFFMM"} 42 | assert info(images["tiffII"], :tiff) == {"image/tiff", 1, 1, "TIFFII"} 43 | end 44 | end 45 | -------------------------------------------------------------------------------- /test/ex_image_info_test/mocks/webp_test.exs: -------------------------------------------------------------------------------- 1 | defmodule ExImageInfoTest.Mocks.WEBPTest do 2 | use ExUnit.Case, async: true 3 | 4 | import ExImageInfo 5 | 6 | setup_all do 7 | images = %{ 8 | "webpVP8" => << 9 | "RIFF", 10 | 0::size(32), 11 | "WEBP", 12 | "VP8", 13 | " ", 14 | 0::size(32), 15 | # 0x22, # not 0x2f 16 | 0x70, 17 | 0::size(40), 18 | # 0x7b00::size(16), 19 | 134::little-size(16), 20 | 457::little-size(16) 21 | # 0xc801::size(16), 22 | # 0x005f::little-size(16), 23 | # 0x00ff::little-size(16), 24 | # 0x0f::little-size(8), 25 | # 0x03::little-size(8), 26 | # 0x07, 27 | # 0x04::little-size(8), 28 | >>, 29 | "webpVP8L" => << 30 | "RIFF", 31 | 0::size(32), 32 | "WEBP", 33 | "VP8L", 34 | 0::size(32), 35 | # 0x22, # not 0x2f 36 | # 0::size(24), 37 | # 0x70, 38 | # 0::size(40), 39 | # 0x9d, 40 | # 0x01, 41 | # 0x22, # 0x2a is invalid 42 | # 0x7b00::size(16), 43 | # 134::little-size(16), 44 | # 457::little-size(16), 45 | 0x2F, 46 | 0x7A, 47 | 0xC0, 48 | 0x71, 49 | 0x00, 50 | 0x35, 51 | 0x86, 52 | 0x83, 53 | 0xB6, 54 | 0x8D 55 | >>, 56 | "webpVP8Linv" => << 57 | "RIFF", 58 | 0::size(32), 59 | "WEBP", 60 | "VP8L", 61 | 0::size(32), 62 | # 0x22, # not 0x2f 63 | 0::size(24), 64 | # 0x70, 65 | # 0::size(40), 66 | 0x9D, 67 | 0x01, 68 | 0x2A, 69 | # 0x7b00::size(16), 70 | 134::little-size(16), 71 | 457::little-size(16) 72 | >> 73 | } 74 | 75 | {:ok, images} 76 | end 77 | 78 | test "force - webp binary mock - #seems? #type #info", images do 79 | assert seems?(images["webpVP8"], :webp) == true 80 | assert seems?(images["webpVP8L"], :webp) == true 81 | assert type(images["webpVP8"], :webp) == {"image/webp", "webpVP8"} 82 | assert type(images["webpVP8L"], :webp) == {"image/webp", "webpVP8L"} 83 | assert info(images["webpVP8"], :webp) == {"image/webp", 134, 457, "webpVP8"} 84 | assert info(images["webpVP8L"], :webp) == {"image/webp", 123, 456, "webpVP8L"} 85 | end 86 | end 87 | -------------------------------------------------------------------------------- /test/fixtures/images/valid/bmp/layers.bmp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Group4Layers/ex_image_info/f9c8315ae562372c08c20e883683057782ecca5a/test/fixtures/images/valid/bmp/layers.bmp -------------------------------------------------------------------------------- /test/fixtures/images/valid/gif/layers-87a.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Group4Layers/ex_image_info/f9c8315ae562372c08c20e883683057782ecca5a/test/fixtures/images/valid/gif/layers-87a.gif -------------------------------------------------------------------------------- /test/fixtures/images/valid/gif/layers-anim.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Group4Layers/ex_image_info/f9c8315ae562372c08c20e883683057782ecca5a/test/fixtures/images/valid/gif/layers-anim.gif -------------------------------------------------------------------------------- /test/fixtures/images/valid/gif/layers.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Group4Layers/ex_image_info/f9c8315ae562372c08c20e883683057782ecca5a/test/fixtures/images/valid/gif/layers.gif -------------------------------------------------------------------------------- /test/fixtures/images/valid/ico/w.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Group4Layers/ex_image_info/f9c8315ae562372c08c20e883683057782ecca5a/test/fixtures/images/valid/ico/w.ico -------------------------------------------------------------------------------- /test/fixtures/images/valid/ico/wikipedia.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Group4Layers/ex_image_info/f9c8315ae562372c08c20e883683057782ecca5a/test/fixtures/images/valid/ico/wikipedia.ico -------------------------------------------------------------------------------- /test/fixtures/images/valid/isobmff/layers-min.heic: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Group4Layers/ex_image_info/f9c8315ae562372c08c20e883683057782ecca5a/test/fixtures/images/valid/isobmff/layers-min.heic -------------------------------------------------------------------------------- /test/fixtures/images/valid/isobmff/layers-rotated.heic: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Group4Layers/ex_image_info/f9c8315ae562372c08c20e883683057782ecca5a/test/fixtures/images/valid/isobmff/layers-rotated.heic -------------------------------------------------------------------------------- /test/fixtures/images/valid/isobmff/layers.avif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Group4Layers/ex_image_info/f9c8315ae562372c08c20e883683057782ecca5a/test/fixtures/images/valid/isobmff/layers.avif -------------------------------------------------------------------------------- /test/fixtures/images/valid/isobmff/layers.heic: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Group4Layers/ex_image_info/f9c8315ae562372c08c20e883683057782ecca5a/test/fixtures/images/valid/isobmff/layers.heic -------------------------------------------------------------------------------- /test/fixtures/images/valid/isobmff/layers.heif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Group4Layers/ex_image_info/f9c8315ae562372c08c20e883683057782ecca5a/test/fixtures/images/valid/isobmff/layers.heif -------------------------------------------------------------------------------- /test/fixtures/images/valid/jp2/layers.jp2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Group4Layers/ex_image_info/f9c8315ae562372c08c20e883683057782ecca5a/test/fixtures/images/valid/jp2/layers.jp2 -------------------------------------------------------------------------------- /test/fixtures/images/valid/jpeg/layers-progressive.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Group4Layers/ex_image_info/f9c8315ae562372c08c20e883683057782ecca5a/test/fixtures/images/valid/jpeg/layers-progressive.jpeg -------------------------------------------------------------------------------- /test/fixtures/images/valid/jpeg/layers.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Group4Layers/ex_image_info/f9c8315ae562372c08c20e883683057782ecca5a/test/fixtures/images/valid/jpeg/layers.jpeg -------------------------------------------------------------------------------- /test/fixtures/images/valid/png/layers.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Group4Layers/ex_image_info/f9c8315ae562372c08c20e883683057782ecca5a/test/fixtures/images/valid/png/layers.png -------------------------------------------------------------------------------- /test/fixtures/images/valid/pnm/layers.pbm: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Group4Layers/ex_image_info/f9c8315ae562372c08c20e883683057782ecca5a/test/fixtures/images/valid/pnm/layers.pbm -------------------------------------------------------------------------------- /test/fixtures/images/valid/pnm/layers.pgm: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Group4Layers/ex_image_info/f9c8315ae562372c08c20e883683057782ecca5a/test/fixtures/images/valid/pnm/layers.pgm -------------------------------------------------------------------------------- /test/fixtures/images/valid/pnm/layers.ppm: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Group4Layers/ex_image_info/f9c8315ae562372c08c20e883683057782ecca5a/test/fixtures/images/valid/pnm/layers.ppm -------------------------------------------------------------------------------- /test/fixtures/images/valid/pnm/plain.pbm: -------------------------------------------------------------------------------- 1 | P1 2 | # comment 3 | # more comment 4 | 3 # width 5 | 11 # height 6 | 0 0 0 7 | 1 0 0 8 | 1 1 0 9 | 1 0 1 10 | 1 0 0 11 | 1 1 0 12 | 1 0 1 13 | 1 0 0 14 | 1 0 1 15 | 1 1 0 16 | 1 1 1 17 | -------------------------------------------------------------------------------- /test/fixtures/images/valid/pnm/plain.pgm: -------------------------------------------------------------------------------- 1 | P2 #comment 2 | 3 11#comment 3 | 5 4 | 0 0 0 5 | 1 0 2 6 | 3 0 5 7 | 2 1 2 8 | 3 0 0 9 | 4 4 4 10 | 4 4 0 11 | 1 4 1 12 | 2 3 2 13 | 1 0 2 14 | 3 0 5 15 | -------------------------------------------------------------------------------- /test/fixtures/images/valid/pnm/plain.ppm: -------------------------------------------------------------------------------- 1 | P3 2 | 2 5 3 | 255 4 | # row 1 5 | 255 0 0 6 | 10 10 80 7 | 8 | 255 0 0 9 | 10 10 20 10 | 11 | 255 0 0 12 | 0 0 255 13 | 14 | 255 0 0 15 | 0 125 125 16 | 17 | 255 0 0 18 | 255 0 0 19 | # row 2 20 | 255 0 255 21 | 0 0 0 22 | 23 | 255 255 255 24 | 0 0 0 25 | 26 | 255 255 0 27 | 0 0 0 28 | 29 | 255 0 255 30 | 0 0 0 31 | 32 | 255 0 0 33 | 0 0 0 34 | -------------------------------------------------------------------------------- /test/fixtures/images/valid/psd/layers.psd: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Group4Layers/ex_image_info/f9c8315ae562372c08c20e883683057782ecca5a/test/fixtures/images/valid/psd/layers.psd -------------------------------------------------------------------------------- /test/fixtures/images/valid/tiff/i-s-big-endian.tiff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Group4Layers/ex_image_info/f9c8315ae562372c08c20e883683057782ecca5a/test/fixtures/images/valid/tiff/i-s-big-endian.tiff -------------------------------------------------------------------------------- /test/fixtures/images/valid/tiff/layers-deflate.tiff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Group4Layers/ex_image_info/f9c8315ae562372c08c20e883683057782ecca5a/test/fixtures/images/valid/tiff/layers-deflate.tiff -------------------------------------------------------------------------------- /test/fixtures/images/valid/tiff/layers-jpeg.tiff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Group4Layers/ex_image_info/f9c8315ae562372c08c20e883683057782ecca5a/test/fixtures/images/valid/tiff/layers-jpeg.tiff -------------------------------------------------------------------------------- /test/fixtures/images/valid/tiff/layers-lzw.tiff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Group4Layers/ex_image_info/f9c8315ae562372c08c20e883683057782ecca5a/test/fixtures/images/valid/tiff/layers-lzw.tiff -------------------------------------------------------------------------------- /test/fixtures/images/valid/tiff/layers-packbits.tiff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Group4Layers/ex_image_info/f9c8315ae562372c08c20e883683057782ecca5a/test/fixtures/images/valid/tiff/layers-packbits.tiff -------------------------------------------------------------------------------- /test/fixtures/images/valid/tiff/layers.tiff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Group4Layers/ex_image_info/f9c8315ae562372c08c20e883683057782ecca5a/test/fixtures/images/valid/tiff/layers.tiff -------------------------------------------------------------------------------- /test/fixtures/images/valid/webp/layers-anim.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Group4Layers/ex_image_info/f9c8315ae562372c08c20e883683057782ecca5a/test/fixtures/images/valid/webp/layers-anim.webp -------------------------------------------------------------------------------- /test/fixtures/images/valid/webp/layers-lossless.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Group4Layers/ex_image_info/f9c8315ae562372c08c20e883683057782ecca5a/test/fixtures/images/valid/webp/layers-lossless.webp -------------------------------------------------------------------------------- /test/fixtures/images/valid/webp/layers-lossy.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Group4Layers/ex_image_info/f9c8315ae562372c08c20e883683057782ecca5a/test/fixtures/images/valid/webp/layers-lossy.webp -------------------------------------------------------------------------------- /test/fixtures/mocks/isobmff.exs: -------------------------------------------------------------------------------- 1 | defmodule ExImageInfoTest.Fixtures.Mocks.ISOBMFF do 2 | @moduledoc "HEIF, HEIC and AVIF binary mocks." 3 | 4 | def images do 5 | %{ 6 | "heic" => << 7 | 0x00000018::32, 8 | "ftyp", #0x66747970::32 9 | "heic", # 0x68656963::32, 10 | 0x00000000::32, 11 | 0x68656963::32, 0x6D696631::32, 12 | # first box (ftyp) 13 | 0x000001EC::32, 0x6D657461::32, 14 | 0x00000000::32, 0x00000021::32, 0x68646C72::32, 0x00000000::32, 15 | 0x00000000::32, 0x70696374::32, 0x00000000::32, 0x00000000::32, 16 | 0x00000000::32, 0x00000000::32, 0x2464696E::32, 0x66000000::32, 17 | 0x1C647265::32, 0x66000000::32, 0x00000000::32, 0x01000000::32, 18 | 0x0C75726C::32, 0x20000000::32, 0x01000000::32, 0x0E706974::32, # "pit" 19 | 0x6D000000::32, 0x00000100::32, # end of pitm box at 01 20 | 0x00003869::32, 0x696E6600::32, 21 | 0x00000000::32, 0x02000000::32, 0x15696E66::32, 0x65020000::32, 22 | 0x00000100::32, 0x00687663::32, 0x31000000::32, 0x0015696E::32, 23 | 0x66650200::32, 0x00010002::32, 0x00004578::32, 0x69660000::32, 24 | 0x00001A69::32, 0x72656600::32, 0x00000000::32, 0x00000E63::32, 25 | 0x64736300::32, 0x02000100::32, 0x01000001::32, 0x0F697072::32, 26 | 0x70000000::32, 0xED697063::32, 0x6F000000::32, 0x13636F6C::32, 27 | 0x726E636C::32, 0x78000200::32, 0x02000680::32, 0x0000000C::32, 28 | 0x636C6C69::32, 0x00CB0040::32, 0x00000014::32, 0x69737065::32, 29 | 0x00000000::32, 0x0000000E::32, 0x00000004::32, 0x00000028::32, 30 | 0x636C6170::32, 0x0000000D::32, 0x00000001::32, 0x00000004::32, 31 | 0x00000001::32, 0xFFC00000::32, 0x00800000::32, 0x00000000::32, 32 | 0x00000001::32, 0x00000009::32, 0x69726F74::32, 0x00000000::32, 33 | 0x10706978::32, 0x69000000::32, 0x00030808::32, 0x08000000::32, 34 | 0x71687663::32, 0x43010370::32, 0x000000B0::32, 0x00000000::32, 35 | 0x001EF000::32, 0xFCFDF8F8::32, 0x00000B03::32, 0xA0000100::32, 36 | 0x1740010C::32, 0x01FFFF03::32, 0x70000003::32, 0x00B00000::32, 37 | 0x03000003::32, 0x001E7024::32, 0xA1000100::32, 0x23420101::32, 38 | 0x03700000::32, 0x0300B000::32, 0x00030000::32, 0x03001EA0::32, 39 | 0x142041C0::32, 0x950FE21E::32, 0xE45954DC::32, 0x08081802::32, 40 | 0xA2000100::32, 0x094401C0::32, 0x6172C844::32, 0x53640000::32, 41 | 0x001A6970::32, 0x6D610000::32, 0x00000000::32, 0x00010001::32, 42 | 0x07810203::32, 0x84850687::32, 0x0000002C::32, 0x696C6F63::32, 43 | 0x00000000::32, 0x44000002::32, 0x00010000::32, 0x00010000::32, 44 | 0x02600000::32, 0x008A0002::32, 0x00000001::32, 0x00000214::32, 45 | 0x0000004C::32, 0x00000001::32, 0x6D646174::32, 0x00000000::32, 46 | 0x000000E6::32, 0x00000006::32, 0x45786966::32, 0x00004D4D::32, 47 | 0x002A0000::32, 0x00080003::32, 0x01120003::32, 0x00000001::32, 48 | 0x00010000::32, 0x011A0005::32, 0x00000001::32, 0x00000032::32, 49 | 0x011B0005::32, 0x00000001::32, 0x0000003A::32, 0x00000000::32, 50 | 0x00000048::32, 0x00000001::32, 0x00000048::32, 0x00000001::32, 51 | 0x00000086::32, 0x2801AFA3::32, 0xF88010D4::32, 0x8A8D7FF9::32, 52 | 0x7431858E::32, 0x8ADC0404::32, 0x77A2E617::32, 0x3190E99C::32, 53 | 0x079BFFAF::32, 0x302F99D8::32, 0xC0E3D4CD::32, 0x121DD65D::32, 54 | 0xF49D5B5E::32, 0xEA51213B::32, 0xFFFFA497::32, 0x8427762A::32, 55 | 0x77DE4B45::32, 0xAA3A060E::32, 0x621C6A2E::32, 0xE4C644FA::32, 56 | 0x06CF7E5F::32, 0x790A0E5D::32, 0x0552E88B::32, 0x7F8006A3::32, 57 | 0x047D3D16::32, 0x8F5D6CFB::32, 0x3DDE8AAF::32, 0x5CA393CE::32, 58 | 0x908DD2BB::32, 0xE38B7FB6::32, 0x0AC6C93F::32, 0xBB248057::32, 59 | 0x0C9A2036::32, 0x2FA20851::32, 0xD36E::32 60 | >>, 61 | "heic-ispe" => << 62 | 0x00000018::32, 63 | "ftyp", #0x66747970::32 64 | "heic", # 0x68656963::32, 65 | 0x00000000::32, 66 | 0x68656963::32, 0x6D696631::32, 67 | # first box (ftyp) 68 | 0x000001EC::32, 0x6D657461::32, 69 | 0x00000000::32, 0x00000021::32, 0x68646C72::32, 0x00000000::32, 70 | 0x00000000::32, 0x70696374::32, 0x00000000::32, 0x00000000::32, 71 | 0x00000000::32, 0x00000000::32, 0x2464696E::32, 0x66000000::32, 72 | 0x1C647265::32, 0x66000000::32, 0x00000000::32, 0x01000000::32, 73 | 0x0C75726C::32, 0x20000000::32, 0x01000000::32, 0x0E706974::32, 74 | 0x6D000000::32, 0x00000100::32, 0x00003869::32, 0x696E6600::32, 75 | 0x00000000::32, 0x02000000::32, 0x15696E66::32, 0x65020000::32, 76 | 0x00000100::32, 0x00687663::32, 0x31000000::32, 0x0015696E::32, 77 | 0x66650200::32, 0x00010002::32, 0x00004578::32, 0x69660000::32, 78 | 0x00001A69::32, 0x72656600::32, 0x00000000::32, 0x00000E63::32, 79 | 0x64736300::32, 0x02000100::32, 0x01000001::32, 0x0F697072::32, 80 | 0x70000000::32, 0xED697063::32, 0x6F000000::32, 0x13636F6C::32, 81 | 0x726E636C::32, 0x78000200::32, 0x02000680::32, 0x0000000C::32, 82 | 0x636C6C69::32, 0x00CB0040::32, 0x00000004::32, "ispe", 83 | # 0x00000000::32, 0x0000000E::32, 0x00000004::32, 0x00000028::32, 84 | >>, 85 | "heic-extended-size-and-wrong-primary-box" => << 86 | # tried using extended size for the first box, and it does not work 87 | # because the size(64) overlaps with the "brand" in the first 4 bytes 88 | 0x00000018::32, 89 | "ftyp", #0x66747970::32 90 | "heic", # 0x68656963::32, 91 | # it seems it cannot be here the extended size 92 | # 0x0000000000000018::size(64), 93 | 0x00000000::32, 94 | 0x68656963::32, 0x6D696631::32, 95 | # first box done (ftyp) 96 | 0x000001EC::32, 0x6D657461::32, 97 | 0x00000000::32, 0x00000021::32, "hdlr", 0x00000000::32, 98 | 0x00000000::32, "pict", 0x00000000::32, 0x00000000::32, 99 | 0x00000000::32, 0x00000000::32, 0x2464696E::32, 0x66000000::32, 100 | 0x1C647265::32, 0x66000000::32, 0x00000000::32, 0x01000000::32, 101 | 0x0C75726C::32, 0x20000000::32, 0x01::8, 102 | # starts pitm box 103 | 0x00000001::32, # 1 == extended size 104 | "pitm", # 0x7069746D::32, 105 | (4 + 4 + 8 + 6)::64, # extended size, has to read until end of pitm 106 | 0x000000::24, 0x0000::16, 2::8, # changing the primary_box to 2 (will be invalid) 107 | # ends pitm box 108 | 0x00::8, 0x00003869::32, 0x696E6600::32, 109 | 0x00000000::32, 0x02000000::32, 0x15696E66::32, 0x65020000::32, 110 | 0x00000100::32, 0x00687663::32, 0x31000000::32, 0x0015696E::32, 111 | 0x66650200::32, 0x00010002::32, 0x00004578::32, 0x69660000::32, 112 | 0x00001A69::32, 0x72656600::32, 0x00000000::32, 0x00000E63::32, 113 | 0x64736300::32, 0x02000100::32, 0x01000001::32, 0x0F697072::32, 114 | 0x70000000::32, 0xED697063::32, 0x6F000000::32, 0x13636F6C::32, 115 | 0x726E636C::32, 0x78000200::32, 0x02000680::32, 0x0000000C::32, 116 | 0x636C6C69::32, 0x00CB0040::32, 0x00000014::32, 0x69737065::32, 117 | 0x00000000::32, 0x0000000E::32, 0x00000004::32, 0x00000028::32, 118 | 0x636C6170::32, 0x0000000D::32, 0x00000001::32, 0x00000004::32, 119 | 0x00000001::32, 0xFFC00000::32, 0x00800000::32, 0x00000000::32, 120 | 0x00000001::32, 0x00000009::32, 0x69726F74::32, 0x00000000::32, 121 | 0x10706978::32, 0x69000000::32, 0x00030808::32, 0x08000000::32, 122 | 0x71687663::32, 0x43010370::32, 0x000000B0::32, 0x00000000::32, 123 | 0x001EF000::32, 0xFCFDF8F8::32, 0x00000B03::32, 0xA0000100::32, 124 | 0x1740010C::32, 0x01FFFF03::32, 0x70000003::32, 0x00B00000::32, 125 | 0x03000003::32, 0x001E7024::32, 0xA1000100::32, 0x23420101::32, 126 | 0x03700000::32, 0x0300B000::32, 0x00030000::32, 0x03001EA0::32, 127 | 0x142041C0::32, 0x950FE21E::32, 0xE45954DC::32, 0x08081802::32, 128 | 0xA2000100::32, 0x094401C0::32, 0x6172C844::32, 0x53640000::32, 129 | 0x001A6970::32, 0x6D610000::32, 0x00000000::32, 0x00010001::32, 130 | 0x07810203::32, 0x84850687::32, 0x0000002C::32, 0x696C6F63::32, 131 | 0x00000000::32, 0x44000002::32, 0x00010000::32, 0x00010000::32, 132 | 0x02600000::32, 0x008A0002::32, 0x00000001::32, 0x00000214::32, 133 | 0x0000004C::32, 0x00000001::32, 0x6D646174::32, 0x00000000::32, 134 | 0x000000E6::32, 0x00000006::32, 0x45786966::32, 0x00004D4D::32, 135 | 0x002A0000::32, 0x00080003::32, 0x01120003::32, 0x00000001::32, 136 | 0x00010000::32, 0x011A0005::32, 0x00000001::32, 0x00000032::32, 137 | 0x011B0005::32, 0x00000001::32, 0x0000003A::32, 0x00000000::32, 138 | 0x00000048::32, 0x00000001::32, 0x00000048::32, 0x00000001::32, 139 | 0x00000086::32, 0x2801AFA3::32, 0xF88010D4::32, 0x8A8D7FF9::32, 140 | 0x7431858E::32, 0x8ADC0404::32, 0x77A2E617::32, 0x3190E99C::32, 141 | 0x079BFFAF::32, 0x302F99D8::32, 0xC0E3D4CD::32, 0x121DD65D::32, 142 | 0xF49D5B5E::32, 0xEA51213B::32, 0xFFFFA497::32, 0x8427762A::32, 143 | 0x77DE4B45::32, 0xAA3A060E::32, 0x621C6A2E::32, 0xE4C644FA::32, 144 | 0x06CF7E5F::32, 0x790A0E5D::32, 0x0552E88B::32, 0x7F8006A3::32, 145 | 0x047D3D16::32, 0x8F5D6CFB::32, 0x3DDE8AAF::32, 0x5CA393CE::32, 146 | 0x908DD2BB::32, 0xE38B7FB6::32, 0x0AC6C93F::32, 0xBB248057::32, 147 | 0x0C9A2036::32, 0x2FA20851::32, 0xD36E::32 148 | >>, 149 | "heic-hdlr-1" => << 150 | 0x00000018::32, 151 | "ftyp", #0x66747970::32 152 | "heic", # 0x68656963::32, 153 | 0x00000000::32, 154 | 0x68656963::32, 0x6D696631::32, 155 | # first box done (ftyp) 156 | 0x000001EC::32, 0x6D657461::32, 157 | 0x00000000::32, 0x00000021::32, "hdlr", 0x00000000::32, 158 | 0x00000000::32, "____", # no "pict" 159 | 0x00000000::32, 0x00000000::32, 160 | 0x00000000::32, 0x00000000::32, 0x2464696E::32, 0x66000000::32, 161 | 0x1C647265::32, 0x66000000::32, 0x00000000::32, 0x01000000::32, 162 | 0x0C75726C::32, 0x20000000::32, 0x01::8, 163 | # starts pitm box 164 | 0x00000001::32, # 1 == extended size 165 | "pitm", # 0x7069746D::32, 166 | (4 + 4 + 8 + 6)::64, # extended size, has to read until end of pitm 167 | 0x000000::24, 0x0000::16, 1::8, # valid primary_box 168 | # ends pitm box 169 | # When supported, continue placing the contents of the above image: 170 | # 0x00::8, 0x00003869::32, 0x696E6600::32, ... 171 | >>, 172 | "heic-hdlr-2" => << 173 | 0x00000018::32, 174 | "ftyp", #0x66747970::32 175 | "heic", # 0x68656963::32, 176 | 0x00000000::32, 177 | 0x68656963::32, 0x6D696631::32, 178 | # first box done (ftyp) 179 | 0x000001EC::32, 0x6D657461::32, 180 | 0x00000000::32, 0x00000009::32, "hdlr", 0x00000000::32, 181 | 0x00000000::32, "____", # no "pict" 182 | 0x00000000::32, 0x00000000::32, 183 | 0x00000000::32, 0x00000000::32, 0x2464696E::32, 0x66000000::32, 184 | 0x1C647265::32, 0x66000000::32, 0x00000000::32, 0x01000000::32, 185 | 0x0C75726C::32, 0x20000000::32, 0x01::8, 186 | # starts pitm box 187 | 0x00000001::32, # 1 == extended size 188 | "pitm", # 0x7069746D::32, 189 | (4 + 4 + 8 + 6)::64, # extended size, has to read until end of pitm 190 | 0x000000::24, 0x0000::16, 1::8, # valid primary_box 191 | # ends pitm box 192 | # 0x00::8, 0x00003869::32, 0x696E6600::32, ... 193 | >>, 194 | "heic-jxlc" => << 195 | 0x00000018::32, 196 | "ftyp", #0x66747970::32 197 | "heic", # 0x68656963::32, 198 | 0x00000000::32, 199 | 0x68656963::32, 0x6D696631::32, 200 | # first box done (ftyp) 201 | 0x000001EC::32, 0x6D657461::32, 202 | 0x00000000::32, 0x00000009::32, "jxlc", 0x00000000::32 203 | >>, 204 | "heic-meta" => << 205 | 0x00000018::32, 206 | "ftyp", #0x66747970::32 207 | "heic", # 0x68656963::32, 208 | 0x00000000::32, 209 | 0x68656963::32, 0x6D696631::32, 210 | # first box done (ftyp) 211 | 0x00000002::32, # meta size < 4 212 | "meta", 213 | 0x00000000::32, 0x00000009::32 214 | >>, 215 | "heif" => << 216 | 0x00000018::32, "ftyp", "mif1", 0x00000000::32, 217 | 0x6D696631::32, 0x68656963::32, 0x000001FE::32, 0x6D657461::32, 218 | 0x00000000::32, 0x00000021::32, 0x68646C72::32, 0x00000000::32, 219 | 0x00000000::32, 0x70696374::32, 0x00000000::32, 0x00000000::32, 220 | 0x00000000::32, 0x00000000::32, 0x0E706974::32, 0x6D000000::32, 221 | 0x0003EA00::32, 0x00003469::32, 0x6C6F6300::32, 0x00000044::32, 222 | 0x40000203::32, 0xEA000000::32, 0x00021600::32, 0x01000000::32, 223 | 0x0800046A::32, 0x8003ED00::32, 0x00000002::32, 0x16000100::32, 224 | 0x046A8800::32, 0x000E4A00::32, 0x00004C69::32, 0x696E6600::32, 225 | 0x00000000::32, 0x02000000::32, 0x1F696E66::32, 0x65020000::32, 226 | 0x0003EA00::32, 0x00687663::32, 0x31484556::32, 0x4320496D::32, 227 | 0x61676500::32, 0x0000001F::32, 0x696E6665::32, 0x02000000::32, 228 | 0x03ED0000::32, 0x68766331::32, 0x48455643::32, 0x20496D61::32, 229 | 0x67650000::32, 0x00001A69::32, 0x72656600::32, 0x00000000::32, 230 | 0x00000E74::32, 0x686D6203::32, 0xED000103::32, 0xEA000001::32, 231 | 0x29697072::32, 0x70000001::32, 0x07697063::32, 0x6F000000::32, 232 | 0x6C687663::32, 0x43010160::32, 0x00000000::32, 0x00000000::32, 233 | 0x00BAF000::32, 0xFCFDF8F8::32, 0x00000F03::32, 0xA0000100::32, 234 | 0x1840010C::32, 0x01FFFF01::32, 0x60000003::32, 0x00000300::32, 235 | 0x00030000::32, 0x0300BAF0::32, 0x24A10001::32, 0x001F4201::32, 236 | 0x01016000::32, 0x00030000::32, 0x03000003::32, 0x00000300::32, 237 | 0xBAA002D0::32, 0x803C1FE5::32, 0xF9246D9E::32, 0xD9A20001::32, 238 | 0x00074401::32, 0xC1909581::32, 0x12000000::32, 0x14697370::32, 239 | 0x65000000::32, 0x00000000::32, 0x0D000000::32, 0x04000000::32, 240 | 0x6B687663::32, 0x43010160::32, 0x00000000::32, 0x00000000::32, 241 | 0x00BAF000::32, 0xFCFDF8F8::32, 0x00000F03::32, 0xA0000100::32, 242 | 0x1840010C::32, 0x01FFFF01::32, 0x60000003::32, 0x00000300::32, 243 | 0x00030000::32, 0x0300BAF0::32, 0x24A10001::32, 0x001E4201::32, 244 | 0x01016000::32, 0x00030000::32, 0x03000003::32, 0x00000300::32, 245 | 0xBAA01E20::32, 0x287F97E4::32, 0x91B67B64::32, 0xA2000100::32, 246 | 0x074401C1::32, 0x90958112::32, 0x00000014::32, 0x69737065::32, 247 | 0x00000000::32, 0x000000F0::32, 0x000000A0::32, 0x0000001A::32, 248 | 0x69706D61::32, 0x00000000::32, 0x00000002::32, 0x03EA0281::32, 249 | 0x0203ED02::32, 0x83040004::32, 0x78D2::32 250 | >>, 251 | "avif" => << 252 | 0x0000001C::32, "ftyp", "avif", 0x00000000::32, 253 | 0x61766966::32, 0x6D696631::32, 0x6D696166::32, 0x000000EA::32, 254 | 0x6D657461::32, 0x00000000::32, 0x00000021::32, 0x68646C72::32, 255 | 0x00000000::32, 0x00000000::32, 0x70696374::32, 0x00000000::32, 256 | 0x00000000::32, 0x00000000::32, 0x00000000::32, 0x0E706974::32, 257 | 0x6D000000::32, 0x00000100::32, 0x00002269::32, 0x6C6F6300::32, 258 | 0x00000044::32, 0x40000100::32, 0x01000000::32, 0x00010E00::32, 259 | 0x01000000::32, 0x00000003::32, 0x0F000000::32, 0x2369696E::32, 260 | 0x66000000::32, 0x00000100::32, 0x00001569::32, 0x6E666502::32, 261 | 0x00000000::32, 0x01000061::32, 0x76303100::32, 0x0000006A::32, 262 | 0x69707270::32, 0x0000004B::32, 0x6970636F::32, 0x0000000C::32, 263 | 0x61763143::32, 0x81000C00::32, 0x00000013::32, 0x636F6C72::32, 264 | 0x6E636C78::32, 0x0001000D::32, 0x00068000::32, 0x00001469::32, 265 | 0x73706500::32, 0x00000000::32, 0x00008200::32, 0x00002A00::32, 266 | 0x00001070::32, 0x69786900::32, 0x00000003::32, 0x08080800::32, 267 | 0x00001769::32, 0x706D6100::32, 0x00000000::32, 0x00000100::32, 268 | 0x01048102::32, 0x03040000::32, 0x03::32 269 | >>, 270 | "truncated-not-readable" => << 271 | 0x0000001C::32, "ftyp", "avif", 0x00000000::32, 272 | 0x61766966::32, 0x6D696631::32, 0x6D696166::32, 12::32, # truncated here 273 | "meta", 0x00000000::32, 0x00000021::32, 0x68646C72::32, 274 | 0x00000000::32, 0x00000000::32, 0x70696374::32, 0x00000000::32, 275 | 0x00000000::32, 0x00000000::32, 0x00000000::32, 0x0E706974::32, 276 | 0x6D000000::32, 0x00000100::32, 0x00002269::32, 0x6C6F6300::32, 277 | 0x00000044::32, 0x40000100::32, 0x01000000::32, 0x00010E00::32, 278 | 0x01000000::32, 0x00000003::32, 0x0F000000::32, 0x2369696E::32, 279 | 0x66000000::32, 0x00000100::32, 0x00001569::32, 0x6E666502::32, 280 | 0x00000000::32, 0x01000061::32, 0x76303100::32, 0x0000006A::32, 281 | 0x69707270::32, 0x0000004B::32, 0x6970636F::32, 0x0000000C::32, 282 | 0x61763143::32, 0x81000C00::32, 0x00000013::32, 0x636F6C72::32, 283 | 0x6E636C78::32, 0x0001000D::32, 0x00068000::32, 0x00001469::32, 284 | 0x73706500::32, 0x00000000::32, 0x00008200::32, 0x00002A00::32, 285 | 0x00001070::32, 0x69786900::32, 0x00000003::32, 0x08080800::32, 286 | 0x00001769::32, 0x706D6100::32, 0x00000000::32, 0x00000100::32, 287 | 0x01048102::32, 0x03040000::32, 0x03::32 288 | >> 289 | } 290 | end 291 | end 292 | -------------------------------------------------------------------------------- /test/support/image_test_case.ex: -------------------------------------------------------------------------------- 1 | defmodule ImageTestCase do 2 | @moduledoc false 3 | 4 | defmacro __using__(opts) do 5 | async? = Keyword.get(opts, :async, true) 6 | 7 | quote bind_quoted: [async?: async?] do 8 | use ExUnit.Case, async: async? 9 | 10 | import ImageTestCase 11 | end 12 | end 13 | 14 | @doc "Reads an image giving a relative path to test/fixtures/images" 15 | def read_image(rel_path) do 16 | abs_path = Path.expand(Path.join("test/fixtures/images", rel_path), File.cwd!()) 17 | File.read!(abs_path) 18 | end 19 | end 20 | -------------------------------------------------------------------------------- /test/test_helper.exs: -------------------------------------------------------------------------------- 1 | ExUnit.start() 2 | --------------------------------------------------------------------------------