├── .editorconfig
├── .github
├── dependabot.yml
├── logscan.sh
└── workflows
│ ├── ci.yml
│ ├── gh-pages.yml
│ ├── publish-wiki.yml
│ ├── release.yml
│ └── report.yml
├── .gitignore
├── .gitmodules
├── .npmignore
├── .vscode
├── c_cpp_properties.json
└── settings.json
├── CHANGELOG.md
├── CODE_OF_CONDUCT.md
├── CONTRIBUTING.md
├── LICENSE
├── README.md
├── eslint.config.mjs
├── examples
├── README.md
├── compress.ts
├── decode-from-jpeg-using-loader.ts
├── decode-from-jpeg.ts
├── decode-from-separate-data-using-loader.ts
├── decode-from-separate-data.ts
├── encode-and-compress.ts
├── encode-jpeg-metadata.ts
├── encode.ts
├── globals.d.ts
├── integrated
│ ├── .editorconfig
│ ├── README.md
│ ├── index.html
│ ├── jstest.html
│ ├── ktx.html
│ ├── libs
│ │ └── basis
│ │ │ ├── README.md
│ │ │ ├── basis_transcoder.js
│ │ │ └── basis_transcoder.wasm
│ ├── main.css
│ └── textures
│ │ └── gainmap
│ │ ├── qwantani_puresky_8k-gainmap.jpg
│ │ ├── qwantani_puresky_8k-gainmap.ktx2
│ │ ├── qwantani_puresky_8k.jpg
│ │ ├── qwantani_puresky_8k.json
│ │ ├── qwantani_puresky_8k.ktx2
│ │ ├── spruit_sunrise_1k.hdr
│ │ ├── spruit_sunrise_4k-gainmap.webp
│ │ ├── spruit_sunrise_4k.jpg
│ │ ├── spruit_sunrise_4k.json
│ │ └── spruit_sunrise_4k.webp
├── tsconfig.json
└── worker.ts
├── package-lock.json
├── package.json
├── playwright.config.ts
├── release.config.cjs
├── reports
└── .gitkeep
├── rollup.config.decodeonly.mjs
├── rollup.config.mjs
├── src
├── core
│ ├── QuadRenderer.ts
│ ├── get-data-texture.ts
│ ├── index.ts
│ └── types.ts
├── decode.ts
├── decode
│ ├── decode.ts
│ ├── errors
│ │ ├── GainMapNotFoundError.ts
│ │ └── XMPMetadataNotFoundError.ts
│ ├── extract.ts
│ ├── index.ts
│ ├── loaders
│ │ ├── GainMapLoader.ts
│ │ ├── HDRJPGLoader.ts
│ │ └── LoaderBase.ts
│ ├── materials
│ │ └── GainMapDecoderMaterial.ts
│ ├── types.ts
│ └── utils
│ │ ├── MPFExtractor.ts
│ │ ├── extractXMP.ts
│ │ └── get-html-image-from-blob.ts
├── encode.ts
├── encode
│ ├── compress.ts
│ ├── encode-and-compress.ts
│ ├── encode.ts
│ ├── find-texture-min-max.ts
│ ├── get-gainmap.ts
│ ├── get-sdr-rendition.ts
│ ├── index.ts
│ ├── materials
│ │ ├── GainMapEncoderMaterial.ts
│ │ └── SDRMaterial.ts
│ └── types.ts
├── libultrahdr.ts
├── libultrahdr
│ ├── decode-jpeg-metadata.ts
│ ├── encode-jpeg-metadata.ts
│ └── library.ts
├── tsconfig.json
├── worker-interface.ts
├── worker-types.ts
└── worker.ts
├── tests
├── __snapshots__
│ ├── decode
│ │ ├── decode.test.ts
│ │ │ ├── chromium-material-values.json
│ │ │ └── chromium-render.png
│ │ ├── loaders
│ │ │ ├── GainMapLoader.test.ts
│ │ │ │ └── chromium-render.png
│ │ │ └── HDRJPGLoader.test.ts
│ │ │ │ ├── chromium-render-no-create-image-bitmap.png
│ │ │ │ ├── chromium-render-plain.png
│ │ │ │ └── chromium-render.png
│ │ └── utils
│ │ │ ├── MPFExtractor.test.ts
│ │ │ ├── chromium-01-jpg-gainmap.png
│ │ │ └── chromium-01-jpg-sdr.png
│ │ │ └── extractXMP.test.ts
│ │ │ ├── chromium-extracts-xmp-from-01-jpg-1.txt
│ │ │ ├── chromium-extracts-xmp-from-02-jpg-1.txt
│ │ │ ├── chromium-extracts-xmp-from-03-jpg-1.txt
│ │ │ ├── chromium-extracts-xmp-from-04-jpg-1.txt
│ │ │ ├── chromium-extracts-xmp-from-05-jpg-1.txt
│ │ │ ├── chromium-extracts-xmp-from-06-jpg-1.txt
│ │ │ ├── chromium-extracts-xmp-from-07-jpg-1.txt
│ │ │ ├── chromium-extracts-xmp-from-08-jpg-1.txt
│ │ │ ├── chromium-extracts-xmp-from-09-jpg-1.txt
│ │ │ ├── chromium-extracts-xmp-from-10-jpg-1.txt
│ │ │ ├── chromium-extracts-xmp-from-abandoned-bakery-16k-jpg-1.txt
│ │ │ ├── chromium-extracts-xmp-from-pisa-4k-jpg-1.txt
│ │ │ └── chromium-extracts-xmp-from-spruit-sunrise-4k-jpg-1.txt
│ ├── encode
│ │ ├── encode-and-compress.test.ts
│ │ │ ├── chromium-memorial-exr-encode-result.png
│ │ │ └── chromium-odd-sized-exr-encode-result.png
│ │ ├── encode.test.ts
│ │ │ ├── chromium-memorial-exr-encode-result-custom-params-gainmap.png
│ │ │ ├── chromium-memorial-exr-encode-result-custom-sdr-params.png
│ │ │ ├── chromium-memorial-exr-encode-result-gainmap.png
│ │ │ ├── chromium-memorial-exr-encode-result-with-tone-mapping-ACESFilmicToneMapping.png
│ │ │ ├── chromium-memorial-exr-encode-result-with-tone-mapping-CineonToneMapping.png
│ │ │ ├── chromium-memorial-exr-encode-result-with-tone-mapping-INVALID.png
│ │ │ ├── chromium-memorial-exr-encode-result-with-tone-mapping-LinearToneMapping.png
│ │ │ ├── chromium-memorial-exr-encode-result-with-tone-mapping-ReinhardToneMapping.png
│ │ │ └── chromium-memorial-exr-encode-result.png
│ │ └── find-texture-min-max.test.ts
│ │ │ ├── chromium-finds-max-values-in-exr-1.txt
│ │ │ └── chromium-finds-min-values-in-exr-1.txt
│ └── examples
│ │ └── integrated-example.test.ts
│ │ ├── chromium-initial.png
│ │ ├── chromium-zoomed-in.png
│ │ └── chromium-zoomed-out-from-above.png
├── coverage-comment-template.md
├── decode
│ ├── decode.test.ts
│ ├── decode.ts
│ ├── extract.test.ts
│ ├── extract.ts
│ ├── loaders
│ │ ├── GainMapLoader.test.ts
│ │ ├── HDRJPGLoader.test.ts
│ │ ├── gainmap-loader.ts
│ │ └── hdr-jpg-loader.ts
│ └── utils
│ │ ├── MPFExtractor.test.ts
│ │ ├── MPFExtractor.ts
│ │ ├── extractXMP.test.ts
│ │ └── extractXMP.ts
├── disableBrowserFeatures.ts
├── encode
│ ├── encode-and-compress.test.ts
│ ├── encode-and-compress.ts
│ ├── encode.test.ts
│ ├── encode.ts
│ ├── find-texture-min-max.test.ts
│ └── find-texture-min-max.ts
├── examples
│ └── integrated-example.test.ts
├── files
│ ├── 01.jpg
│ ├── 02.jpg
│ ├── 03.jpg
│ ├── 04.jpg
│ ├── 05.jpg
│ ├── 06.jpg
│ ├── 07.jpg
│ ├── 08.jpg
│ ├── 09.jpg
│ ├── 10.jpg
│ ├── abandoned_bakery_16k.jpg
│ ├── chcaus2-bloom.exr
│ ├── chcaus2-bloom.hdr
│ ├── gray.exr
│ ├── grey.exr
│ ├── invalid_image.png
│ ├── memorial.exr
│ ├── memorial.hdr
│ ├── memorial.jpg
│ ├── odd-sized.exr
│ ├── pisa-4k.jpg
│ ├── plain-jpeg.jpg
│ ├── spruit_sunrise_1k.hdr
│ ├── spruit_sunrise_4k-gainmap.webp
│ ├── spruit_sunrise_4k.jpg
│ ├── spruit_sunrise_4k.json
│ └── spruit_sunrise_4k.webp
├── global.d.ts
├── testWithCoverage.ts
├── testbed.html
└── tsconfig.json
├── tsconfig.json
└── typedoc.config.cjs
/.editorconfig:
--------------------------------------------------------------------------------
1 | root = true
2 |
3 | [*]
4 | charset = utf-8
5 | indent_style = space
6 | indent_size = 2
7 | end_of_line = lf
8 | insert_final_newline = true
9 | trim_trailing_whitespace = true
10 |
--------------------------------------------------------------------------------
/.github/dependabot.yml:
--------------------------------------------------------------------------------
1 | # To get started with Dependabot version updates, you'll need to specify which
2 | # package ecosystems to update and where the package manifests are located.
3 | # Please see the documentation for all configuration options:
4 | # https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates
5 |
6 | version: 2
7 | updates:
8 | - package-ecosystem: "npm" # See documentation for possible values
9 | directory: "/" # Location of package manifests
10 | schedule:
11 | interval: "weekly"
12 | allow:
13 | - dependency-name: "three"
14 |
--------------------------------------------------------------------------------
/.github/logscan.sh:
--------------------------------------------------------------------------------
1 | # /bin/bash
2 | if grep -q error $1; then
3 | echo "errors in $1"
4 | out=$(cat $1)
5 | echo "$out"
6 |
7 | # first `sed` replaces newlines with \n
8 | # seconds `sed` replaces quotes with escaped quotes \"
9 |
10 | echo "summary=$(echo "$out" | sed ':a;N;$!ba;s/\n/\\n/g' | sed 's/"/\\"/g' )" >> "$GITHUB_OUTPUT"
11 | exit 1
12 | fi
13 | out="no errors in $1"
14 | echo "$out"
15 | echo "summary=$out" >> "$GITHUB_OUTPUT"
16 | exit 0
17 |
--------------------------------------------------------------------------------
/.github/workflows/ci.yml:
--------------------------------------------------------------------------------
1 | name: CI
2 |
3 | on:
4 | push:
5 | branches: [ main ]
6 | pull_request:
7 | branches: [ main ]
8 |
9 | jobs:
10 | ################################
11 | ##
12 | ## BUILD
13 | ##
14 | ################################
15 | Build:
16 | name: Build
17 | runs-on: ubuntu-latest
18 | env:
19 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
20 | steps:
21 | - name: 'Checkout'
22 | uses: actions/checkout@v4
23 | with:
24 | submodules: recursive
25 |
26 | - name: 'Setup Emscripten'
27 | uses: mymindstorm/setup-emsdk@v11
28 | with:
29 | version: 3.1.47
30 |
31 | - name: 'Setup Python'
32 | uses: actions/setup-python@v4
33 | with:
34 | python-version: '3.10'
35 |
36 | - name: 'Install Meson & Ninja'
37 | uses: BSFishy/pip-action@v1
38 | with:
39 | packages: |
40 | meson
41 | ninja
42 |
43 | - name: Write em.txt
44 | uses: "DamianReeves/write-file-action@master"
45 | with:
46 | path: libultrahdr-wasm/em.txt
47 | write-mode: overwrite
48 | contents: |
49 | [binaries]
50 | c = 'emcc'
51 | cpp = 'em++'
52 | ar = 'emar'
53 | nm = 'emnm'
54 |
55 | [host_machine]
56 | system = 'emscripten'
57 | cpu_family = 'wasm32'
58 | cpu = 'wasm32'
59 | endian = 'little'
60 |
61 | - name: 'Build libultrahdr WASM'
62 | run: |
63 | cd libultrahdr-wasm
64 | meson setup build --cross-file=em.txt
65 | meson compile -C build
66 |
67 | - name: 'Setup Nodejs'
68 | uses: actions/setup-node@v3
69 | with:
70 | node-version: 20
71 |
72 | - name: 'Install dependencies'
73 | run: npm ci
74 |
75 | - name: 'Build'
76 | run: npm run build
77 |
78 | - name: 'Upload Build artifacts'
79 | if: always()
80 | uses: actions/upload-artifact@v4
81 | with:
82 | name: build-artifact
83 | if-no-files-found: error
84 | path: |
85 | libultrahdr-wasm/build/*.ts
86 | libultrahdr-wasm/build/*.js
87 | libultrahdr-wasm/build/*.map
88 | libultrahdr-wasm/build/*.wasm
89 | dist/
90 |
91 | ################################
92 | ##
93 | ## CHECKS
94 | ##
95 | ################################
96 | Check:
97 | name: Check
98 | needs: Build
99 | runs-on: ubuntu-latest
100 | steps:
101 | - name: 'Checkout'
102 | uses: actions/checkout@v4
103 | with:
104 | submodules: recursive
105 |
106 | - name: 'Download build artifacts'
107 | uses: actions/download-artifact@v4
108 | with:
109 | name: build-artifact
110 |
111 | - name: 'Setup Nodejs'
112 | uses: actions/setup-node@v3
113 | with:
114 | node-version: 20
115 |
116 | - name: 'Install dependencies'
117 | run: npm ci
118 |
119 | - name: 'Produce Reports & Logs'
120 | if: always()
121 | run: npm run ci:check
122 |
123 | - name: 'Upload Check artifacts'
124 | if: always()
125 | uses: actions/upload-artifact@v4
126 | with:
127 | name: check-artifact
128 | if-no-files-found: error
129 | path: |
130 | reports/
131 |
132 | ################################
133 | ##
134 | ## TEST
135 | ##
136 | ################################
137 | Test:
138 | name: Test
139 | needs: Build
140 | runs-on: ubuntu-latest
141 | # container:
142 | # image: mcr.microsoft.com/playwright:v1.40.0-jammy
143 | steps:
144 | # - name: 'Initialize Git LFS'
145 | # run: curl -s https://packagecloud.io/install/repositories/github/git-lfs/script.deb.sh | bash
146 |
147 | # - name: 'Install Git LFS'
148 | # run: apt-get install -y git-lfs
149 |
150 | - name: 'Checkout'
151 | uses: actions/checkout@v4
152 | with:
153 | submodules: recursive
154 |
155 | - name: 'Download build artifacts'
156 | uses: actions/download-artifact@v4
157 | with:
158 | name: build-artifact
159 |
160 | - name: 'Setup Nodejs'
161 | uses: actions/setup-node@v3
162 | with:
163 | node-version: 20
164 |
165 | - name: 'Install dependencies'
166 | run: npm ci
167 |
168 | - name: 'Install playwright Browsers'
169 | run: npx playwright install --with-deps
170 |
171 | - name: 'Run Playwright Tests'
172 | run: npm test
173 | # env:
174 | # HOME: /root
175 |
176 | - name: 'Upload Test artifacts'
177 | if: always()
178 | uses: actions/upload-artifact@v4
179 | with:
180 | name: test-artifact
181 | if-no-files-found: error
182 | path: |
183 | .nyc_output/
184 | test-results/
185 | coverage/
186 | playwright-report/
187 | tests/__snapshots__/
188 |
--------------------------------------------------------------------------------
/.github/workflows/gh-pages.yml:
--------------------------------------------------------------------------------
1 | # Simple workflow for deploying static content to GitHub Pages
2 | name: 'Deploy Threejs Example'
3 |
4 | on:
5 | # Runs on pushes targeting the default branch
6 | push:
7 | branches: [main]
8 |
9 | # Allows you to run this workflow manually from the Actions tab
10 | workflow_dispatch:
11 |
12 | # Sets permissions of the GITHUB_TOKEN to allow deployment to GitHub Pages
13 | permissions:
14 | contents: read
15 | pages: write
16 | id-token: write
17 |
18 | # Allow only one concurrent deployment, skipping runs queued between the run in-progress and latest queued.
19 | # However, do NOT cancel in-progress runs as we want to allow these production deployments to complete.
20 | concurrency:
21 | group: "pages"
22 | cancel-in-progress: false
23 |
24 | jobs:
25 | # Single deploy job since we're just deploying
26 | deploy:
27 | environment:
28 | name: github-pages
29 | url: ${{ steps.deployment.outputs.page_url }}
30 | runs-on: ubuntu-latest
31 | steps:
32 | - name: Checkout
33 | uses: actions/checkout@v3
34 | - name: Setup Pages
35 | uses: actions/configure-pages@v5
36 | - name: Upload artifact
37 | uses: actions/upload-pages-artifact@v3
38 | with:
39 | # Upload entire repository
40 | path: './examples/integrated'
41 | - name: Deploy to GitHub Pages
42 | id: deployment
43 | uses: actions/deploy-pages@v4
44 |
--------------------------------------------------------------------------------
/.github/workflows/publish-wiki.yml:
--------------------------------------------------------------------------------
1 | name: 'Publish wiki'
2 |
3 | on:
4 | workflow_run:
5 | branches: [main]
6 | workflows: [CI]
7 | types:
8 | - completed
9 |
10 | concurrency:
11 | group: publish-wiki
12 | cancel-in-progress: true
13 |
14 | permissions:
15 | contents: write
16 | actions: read
17 |
18 | jobs:
19 | publish-wiki:
20 | name: 'Publish Wiki'
21 | runs-on: ubuntu-latest
22 | if: ${{ github.event.workflow_run.conclusion == 'success' }}
23 | steps:
24 | - name: 'Checkout repo'
25 | uses: actions/checkout@v3
26 | with:
27 | submodules: recursive
28 |
29 | - name: 'Download build artifacts'
30 | uses: dawidd6/action-download-artifact@v6
31 | with:
32 | name: build-artifact
33 | run_id: ${{ github.event.workflow_run.id }}
34 | workflow_conclusion: success
35 |
36 |
37 | - name: 'Setup Nodejs'
38 | uses: actions/setup-node@v3
39 | with:
40 | node-version: 20
41 |
42 | - name: 'Install Packages'
43 | run: npm ci
44 |
45 | - name: 'Produce Wiki'
46 | run: npx typedoc
47 |
48 | - name: 'Publish Wiki'
49 | uses: Andrew-Chen-Wang/github-wiki-action@v4
50 |
--------------------------------------------------------------------------------
/.github/workflows/release.yml:
--------------------------------------------------------------------------------
1 | name: Release
2 |
3 | on:
4 | # Allows you to run this workflow manually from the Actions tab
5 | workflow_dispatch:
6 | # run when tests have passed
7 | workflow_run:
8 | branches: [main]
9 | workflows: [CI]
10 | types:
11 | - completed
12 |
13 | concurrency:
14 | group: release
15 | cancel-in-progress: true
16 |
17 | permissions:
18 | contents: write
19 | issues: write
20 | pull-requests: write
21 | actions: read
22 |
23 | jobs:
24 | release:
25 | name: Release
26 | runs-on: ubuntu-latest
27 | if: ${{ github.event.workflow_run.conclusion == 'success' }}
28 | env:
29 | GITHUB_TOKEN: ${{ secrets.RELEASE_GITHUB_TOKEN }}
30 | NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
31 | steps:
32 | - name: 'Checkout'
33 | uses: nschloe/action-cached-lfs-checkout@v1
34 | with:
35 | submodules: recursive
36 | token: ${{ secrets.RELEASE_GITHUB_TOKEN }}
37 |
38 | - name: 'Download build artifacts'
39 | uses: dawidd6/action-download-artifact@v6 # download artifacts
40 | with:
41 | name: build-artifact
42 | run_id: ${{ github.event.workflow_run.id }}
43 | workflow_conclusion: success
44 |
45 |
46 | - name: 'Setup Nodejs'
47 | uses: actions/setup-node@v3
48 | with:
49 | node-version: 20
50 |
51 | - name: 'Install Packages'
52 | run: npm ci
53 |
54 | - name: 'Release'
55 | run: npx semantic-release
56 |
57 |
--------------------------------------------------------------------------------
/.github/workflows/report.yml:
--------------------------------------------------------------------------------
1 | name: Report on pull requests
2 |
3 |
4 | on:
5 | workflow_run:
6 | workflows: [CI]
7 | types:
8 | - completed
9 |
10 | jobs:
11 | report:
12 | name: Report
13 | runs-on: ubuntu-latest
14 | if: github.event.workflow_run.event == 'pull_request'
15 | env:
16 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
17 | steps:
18 | - name: 'Checkout'
19 | uses: actions/checkout@v4
20 | with:
21 | submodules: recursive
22 |
23 | - name: 'Download build artifacts'
24 | if: always()
25 | uses: dawidd6/action-download-artifact@v6 # download artifacts
26 | with:
27 | name: build-artifact
28 | run_id: ${{ github.event.workflow_run.id }}
29 |
30 | - name: 'Download check artifacts'
31 | if: always()
32 | uses: dawidd6/action-download-artifact@v6 # download artifacts
33 | with:
34 | name: check-artifact
35 | run_id: ${{ github.event.workflow_run.id }}
36 |
37 | - name: 'Download test artifacts'
38 | if: always()
39 | uses: dawidd6/action-download-artifact@v6 # download artifacts
40 | with:
41 | name: test-artifact
42 | run_id: ${{ github.event.workflow_run.id }}
43 |
44 |
45 | - name: 'Logscan ensure logscan is executable'
46 | if: always()
47 | run: 'chmod +x .github/logscan.sh'
48 |
49 | - name: 'Logscan TypeCheck Src'
50 | if: always()
51 | id: logscan_src
52 | run: '.github/logscan.sh reports/typecheck.log'
53 |
54 | - name: 'Logscan TypeCheck Examples'
55 | if: always()
56 | id: logscan_examples
57 | run: '.github/logscan.sh reports/typecheck-examples.log'
58 |
59 | - name: 'Logscan TypeCheck Tests'
60 | if: always()
61 | id: logscan_tests
62 | run: '.github/logscan.sh reports/typecheck-tests.log'
63 |
64 | - name: 'TypeCheck Src'
65 | if: always()
66 | uses: LouisBrunner/checks-action@v1.6.1
67 | with:
68 | token: ${{ secrets.GITHUB_TOKEN }}
69 | name: Typecheck Src
70 | conclusion: ${{steps.logscan_src.conclusion}}
71 | sha: ${{ github.event.workflow_run.head_sha }}
72 | output: |
73 | {"summary":"${{ steps.logscan_src.outputs.summary }}"}
74 |
75 | - name: 'TypeCheck Examples'
76 | if: always()
77 | uses: LouisBrunner/checks-action@v1.6.1
78 | with:
79 | token: ${{ secrets.GITHUB_TOKEN }}
80 | name: Typecheck Examples
81 | conclusion: ${{steps.logscan_examples.conclusion}}
82 | sha: ${{ github.event.workflow_run.head_sha }}
83 | output: |
84 | {"summary":"${{ steps.logscan_examples.outputs.summary }}"}
85 |
86 | - name: 'TypeCheck Tests'
87 | if: always()
88 | uses: LouisBrunner/checks-action@v1.6.1
89 | with:
90 | token: ${{ secrets.GITHUB_TOKEN }}
91 | name: Typecheck Tests
92 | conclusion: ${{steps.logscan_tests.conclusion}}
93 | sha: ${{ github.event.workflow_run.head_sha }}
94 | output: |
95 | {"summary":"${{ steps.logscan_tests.outputs.summary }}"}
96 |
97 | - name: 'Analyze Src Code Linting Results'
98 | if: always()
99 | id: eslint_src
100 | uses: ataylorme/eslint-annotate-action@v2
101 | with:
102 | report-json: "reports/eslint-src.json"
103 |
104 | - name: 'Analyze Examples Code Linting Results'
105 | if: always()
106 | id: eslint_examples
107 | uses: ataylorme/eslint-annotate-action@v2
108 | with:
109 | report-json: "reports/eslint-examples.json"
110 |
111 | - name: 'Analyze Tests Code Linting Results'
112 | if: always()
113 | id: eslint_tests
114 | uses: ataylorme/eslint-annotate-action@v2
115 | with:
116 | report-json: "reports/eslint-tests.json"
117 |
118 | - name: 'Report Eslint Src'
119 | if: always()
120 | uses: LouisBrunner/checks-action@v1.6.1
121 | with:
122 | token: ${{ secrets.GITHUB_TOKEN }}
123 | name: 'Eslint Src'
124 | conclusion: ${{steps.eslint_src.conclusion}}
125 | sha: ${{ github.event.workflow_run.head_sha }}
126 | output: |
127 | {"summary":"${{ steps.eslint_src.outputs.summary }}"}
128 |
129 | - name: 'Report Eslint Examples'
130 | if: always()
131 | uses: LouisBrunner/checks-action@v1.6.1
132 | with:
133 | token: ${{ secrets.GITHUB_TOKEN }}
134 | name: 'Eslint Examples'
135 | conclusion: ${{steps.eslint_examples.conclusion}}
136 | sha: ${{ github.event.workflow_run.head_sha }}
137 | output: |
138 | {"summary":"${{ steps.eslint_examples.outputs.summary }}"}
139 |
140 | - name: 'Report Eslint Tests'
141 | if: always()
142 | uses: LouisBrunner/checks-action@v1.6.1
143 | with:
144 | token: ${{ secrets.GITHUB_TOKEN }}
145 | name: 'Eslint Tests'
146 | conclusion: ${{steps.eslint_tests.conclusion}}
147 | sha: ${{ github.event.workflow_run.head_sha }}
148 | output: |
149 | {"summary":"${{ steps.eslint_tests.outputs.summary }}"}
150 |
151 |
152 | - name: 'Upload coverage reports to Codecov'
153 | if: always()
154 | uses: codecov/codecov-action@v3
155 | with:
156 | directory: ./coverage/
157 | override_pr: ${{ github.event.workflow_run.pull_requests[0].number }}
158 | override_commit: ${{ github.event.workflow_run.head_sha }}
159 |
160 |
161 | # - name: 'Report playwright test results'
162 | # uses: daun/playwright-report-summary@v2
163 | # with:
164 | # report-file: playwright-report.json
165 | # env:
166 | # GITHUB_SHA: ${{ github.event.workflow_run.head_sha }}
167 | # GITHUB_EVENT_NAME: ${{ github.event.workflow_run.event }}
168 |
169 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | .DS_Store
2 | .thumbs.db
3 | node_modules
4 |
5 | # Log files
6 | npm-debug.log*
7 | yarn-debug.log*
8 | yarn-error.log*
9 |
10 | # Editor directories and files
11 | .idea
12 | *.suo
13 | *.ntvs*
14 | *.njsproj
15 | *.sln
16 | *.tgz
17 |
18 | dist/
19 | doc/
20 | wiki/
21 | junit.xml
22 | reports/*.json
23 | reports/*.xml
24 | reports/*.txt
25 | reports/*.log
26 | test-results
27 | playwright-report
28 | .nyc_output
29 | coverage
30 | playwright-report.json
31 |
--------------------------------------------------------------------------------
/.gitmodules:
--------------------------------------------------------------------------------
1 | [submodule "libultrahdr-wasm"]
2 | path = libultrahdr-wasm
3 | url = git@github.com:MONOGRID/libultrahdr-wasm.git
4 |
--------------------------------------------------------------------------------
/.npmignore:
--------------------------------------------------------------------------------
1 | .*
2 | *.config.*
3 | *.tgz
4 | tests
5 | tsconfig.json
6 | src
7 | reports
8 | examples
9 | wiki
10 | libultrahdr-wasm/**/*
11 | !libultrahdr-wasm/build/*.ts
12 | !libultrahdr-wasm/build/*.js
13 | !libultrahdr-wasm/build/*.wasm
14 |
--------------------------------------------------------------------------------
/.vscode/c_cpp_properties.json:
--------------------------------------------------------------------------------
1 | {
2 | "configurations": [
3 | {
4 | "name": "Emscripten",
5 | "includePath": [
6 | "${workspaceFolder}/**"
7 | ],
8 | "defines": [],
9 | "compileCommands": "${workspaceFolder}/libultrahdr-wasm/build/compile_commands.json"
10 | }
11 | ],
12 | "version": 4
13 | }
14 |
--------------------------------------------------------------------------------
/.vscode/settings.json:
--------------------------------------------------------------------------------
1 | {
2 | "cmake.configureOnOpen": false,
3 | "files.associations": {
4 | "*.controller": "yaml",
5 | "*.anim": "yaml",
6 | "*.asset": "yaml",
7 | "*.mat": "yaml",
8 | "*.prefab": "yaml",
9 | ".modernizrrc": "javascript",
10 | "*.jslib": "javascript",
11 | "*.jspre": "javascript",
12 | "array": "cpp",
13 | "string": "cpp",
14 | "string_view": "cpp",
15 | "vector": "cpp",
16 | "iterator": "cpp",
17 | "atomic": "cpp",
18 | "cstdint": "cpp",
19 | "limits": "cpp",
20 | "__bit_reference": "cpp",
21 | "__config": "cpp",
22 | "__debug": "cpp",
23 | "__errc": "cpp",
24 | "__hash_table": "cpp",
25 | "__locale": "cpp",
26 | "__mutex_base": "cpp",
27 | "__node_handle": "cpp",
28 | "__split_buffer": "cpp",
29 | "__threading_support": "cpp",
30 | "__tree": "cpp",
31 | "__verbose_abort": "cpp",
32 | "bitset": "cpp",
33 | "cctype": "cpp",
34 | "clocale": "cpp",
35 | "cmath": "cpp",
36 | "codecvt": "cpp",
37 | "condition_variable": "cpp",
38 | "cstdarg": "cpp",
39 | "cstddef": "cpp",
40 | "cstdio": "cpp",
41 | "cstdlib": "cpp",
42 | "cstring": "cpp",
43 | "ctime": "cpp",
44 | "cwchar": "cpp",
45 | "cwctype": "cpp",
46 | "deque": "cpp",
47 | "exception": "cpp",
48 | "fstream": "cpp",
49 | "initializer_list": "cpp",
50 | "iomanip": "cpp",
51 | "ios": "cpp",
52 | "iosfwd": "cpp",
53 | "iostream": "cpp",
54 | "istream": "cpp",
55 | "list": "cpp",
56 | "locale": "cpp",
57 | "map": "cpp",
58 | "mutex": "cpp",
59 | "new": "cpp",
60 | "optional": "cpp",
61 | "ostream": "cpp",
62 | "ratio": "cpp",
63 | "regex": "cpp",
64 | "set": "cpp",
65 | "sstream": "cpp",
66 | "stack": "cpp",
67 | "stdexcept": "cpp",
68 | "streambuf": "cpp",
69 | "system_error": "cpp",
70 | "thread": "cpp",
71 | "tuple": "cpp",
72 | "typeinfo": "cpp",
73 | "unordered_map": "cpp",
74 | "variant": "cpp",
75 | "algorithm": "cpp",
76 | "bit": "cpp",
77 | "*.tcc": "cpp",
78 | "chrono": "cpp",
79 | "cinttypes": "cpp",
80 | "compare": "cpp",
81 | "concepts": "cpp",
82 | "unordered_set": "cpp",
83 | "functional": "cpp",
84 | "memory": "cpp",
85 | "memory_resource": "cpp",
86 | "numeric": "cpp",
87 | "random": "cpp",
88 | "type_traits": "cpp",
89 | "utility": "cpp",
90 | "numbers": "cpp",
91 | "semaphore": "cpp",
92 | "shared_mutex": "cpp",
93 | "stop_token": "cpp",
94 | "cfenv": "cpp",
95 | "__nullptr": "cpp",
96 | "*.incl_cpp": "cpp"
97 | },
98 | "C_Cpp.default.compileCommands": "builddir/compile_commands.json",
99 | "mesonbuild.configureOnOpen": false
100 | }
101 |
--------------------------------------------------------------------------------
/CODE_OF_CONDUCT.md:
--------------------------------------------------------------------------------
1 | # Contributor Covenant Code of Conduct
2 |
3 | ## Our Pledge
4 |
5 | We as members, contributors, and leaders pledge to make participation in our
6 | community a harassment-free experience for everyone, regardless of age, body
7 | size, visible or invisible disability, ethnicity, sex characteristics, gender
8 | identity and expression, level of experience, education, socio-economic status,
9 | nationality, personal appearance, race, religion, or sexual identity
10 | and orientation.
11 |
12 | We pledge to act and interact in ways that contribute to an open, welcoming,
13 | diverse, inclusive, and healthy community.
14 |
15 | ## Our Standards
16 |
17 | Examples of behavior that contributes to a positive environment for our
18 | community include:
19 |
20 | * Demonstrating empathy and kindness toward other people
21 | * Being respectful of differing opinions, viewpoints, and experiences
22 | * Giving and gracefully accepting constructive feedback
23 | * Accepting responsibility and apologizing to those affected by our mistakes,
24 | and learning from the experience
25 | * Focusing on what is best not just for us as individuals, but for the
26 | overall community
27 |
28 | Examples of unacceptable behavior include:
29 |
30 | * The use of sexualized language or imagery, and sexual attention or
31 | advances of any kind
32 | * Trolling, insulting or derogatory comments, and personal or political attacks
33 | * Public or private harassment
34 | * Publishing others' private information, such as a physical or email
35 | address, without their explicit permission
36 | * Other conduct which could reasonably be considered inappropriate in a
37 | professional setting
38 |
39 | ## Enforcement Responsibilities
40 |
41 | Community leaders are responsible for clarifying and enforcing our standards of
42 | acceptable behavior and will take appropriate and fair corrective action in
43 | response to any behavior that they deem inappropriate, threatening, offensive,
44 | or harmful.
45 |
46 | Community leaders have the right and responsibility to remove, edit, or reject
47 | comments, commits, code, wiki edits, issues, and other contributions that are
48 | not aligned to this Code of Conduct, and will communicate reasons for moderation
49 | decisions when appropriate.
50 |
51 | ## Scope
52 |
53 | This Code of Conduct applies within all community spaces, and also applies when
54 | an individual is officially representing the community in public spaces.
55 | Examples of representing our community include using an official e-mail address,
56 | posting via an official social media account, or acting as an appointed
57 | representative at an online or offline event.
58 |
59 | ## Enforcement
60 |
61 | Instances of abusive, harassing, or otherwise unacceptable behavior may be
62 | reported to the community leaders responsible for enforcement at
63 | gainmap@monogrid.com.
64 | All complaints will be reviewed and investigated promptly and fairly.
65 |
66 | All community leaders are obligated to respect the privacy and security of the
67 | reporter of any incident.
68 |
69 | ## Enforcement Guidelines
70 |
71 | Community leaders will follow these Community Impact Guidelines in determining
72 | the consequences for any action they deem in violation of this Code of Conduct:
73 |
74 | ### 1. Correction
75 |
76 | **Community Impact**: Use of inappropriate language or other behavior deemed
77 | unprofessional or unwelcome in the community.
78 |
79 | **Consequence**: A private, written warning from community leaders, providing
80 | clarity around the nature of the violation and an explanation of why the
81 | behavior was inappropriate. A public apology may be requested.
82 |
83 | ### 2. Warning
84 |
85 | **Community Impact**: A violation through a single incident or series
86 | of actions.
87 |
88 | **Consequence**: A warning with consequences for continued behavior. No
89 | interaction with the people involved, including unsolicited interaction with
90 | those enforcing the Code of Conduct, for a specified period of time. This
91 | includes avoiding interactions in community spaces as well as external channels
92 | like social media. Violating these terms may lead to a temporary or
93 | permanent ban.
94 |
95 | ### 3. Temporary Ban
96 |
97 | **Community Impact**: A serious violation of community standards, including
98 | sustained inappropriate behavior.
99 |
100 | **Consequence**: A temporary ban from any sort of interaction or public
101 | communication with the community for a specified period of time. No public or
102 | private interaction with the people involved, including unsolicited interaction
103 | with those enforcing the Code of Conduct, is allowed during this period.
104 | Violating these terms may lead to a permanent ban.
105 |
106 | ### 4. Permanent Ban
107 |
108 | **Community Impact**: Demonstrating a pattern of violation of community
109 | standards, including sustained inappropriate behavior, harassment of an
110 | individual, or aggression toward or disparagement of classes of individuals.
111 |
112 | **Consequence**: A permanent ban from any sort of public interaction within
113 | the community.
114 |
115 | ## Attribution
116 |
117 | This Code of Conduct is adapted from the [Contributor Covenant][homepage],
118 | version 2.0, available at
119 | https://www.contributor-covenant.org/version/2/0/code_of_conduct.html.
120 |
121 | Community Impact Guidelines were inspired by [Mozilla's code of conduct
122 | enforcement ladder](https://github.com/mozilla/diversity).
123 |
124 | [homepage]: https://www.contributor-covenant.org
125 |
126 | For answers to common questions about this code of conduct, see the FAQ at
127 | https://www.contributor-covenant.org/faq. Translations are available at
128 | https://www.contributor-covenant.org/translations.
129 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2023 MONOGRID
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/eslint.config.mjs:
--------------------------------------------------------------------------------
1 | import { FlatCompat } from '@eslint/eslintrc'
2 | import { defineConfig } from 'eslint/config'
3 | // @ts-expect-error untyped lib
4 | import mdcs from 'eslint-config-mdcs'
5 | // @ts-expect-error untyped lib
6 | import htmlPlugin from 'eslint-plugin-html'
7 | import simpleImportSort from 'eslint-plugin-simple-import-sort'
8 | import unusedImports from 'eslint-plugin-unused-imports'
9 | import globals from 'globals'
10 | import neoStandard, { plugins } from 'neostandard'
11 | import path from 'path'
12 | import { fileURLToPath } from 'url'
13 |
14 | // HTML examples need a config of their own
15 | const mdcsCompat = new FlatCompat({
16 | baseDirectory: path.dirname(fileURLToPath(import.meta.url))
17 | }).config(mdcs)
18 |
19 | const integratedExamplesPath = 'examples/integrated/*.html'
20 |
21 | // Get the neostandard configs
22 | const neoStandardConfigs = neoStandard({ ts: true })
23 | // Get TypeScript recommended type checking configs
24 | const typeCheckingConfigs = plugins['typescript-eslint'].configs['recommendedTypeChecked']
25 |
26 | const config = defineConfig([
27 | // Common ignores for all configs
28 | {
29 | ignores: [
30 | 'node_modules/**/*',
31 | 'dist/**/*',
32 | '.vscode/**/*',
33 | 'libultrahdr-wasm/build/**/*'
34 | ]
35 | }
36 | ])
37 |
38 | // Add mdcs configs for html files in examples
39 | for (const mdcsConfig of mdcsCompat) {
40 | config.push({
41 | ...mdcsConfig,
42 | files: [integratedExamplesPath],
43 | languageOptions: {
44 | globals: {
45 | ...globals.browser,
46 | },
47 | parserOptions: {
48 | extraFileExtensions: ['.html'],
49 | }
50 | },
51 | plugins: {
52 | html: htmlPlugin
53 | }
54 | })
55 | }
56 |
57 | // Add neostandard configs with proper file patterns
58 | for (const neoConfig of neoStandardConfigs) {
59 | config.push({
60 | ...neoConfig,
61 | files: ['**/*.{ts,mts,cts,tsx,js,mjs,cjs}'],
62 | ignores: [
63 | ...(neoConfig.ignores || []),
64 | integratedExamplesPath
65 | ]
66 | })
67 | }
68 |
69 | // Add TypeScript type checking configs
70 | if (Array.isArray(typeCheckingConfigs)) {
71 | for (const tsConfig of typeCheckingConfigs) {
72 | // @ts-expect-error untyped lib
73 | config.push({
74 | ...tsConfig,
75 | files: ['**/*.{ts,mts,cts,tsx}'],
76 | ignores: [
77 | ...(tsConfig.ignores || []),
78 | integratedExamplesPath
79 | ]
80 | })
81 | }
82 | } else {
83 | config.push({
84 | // @ts-expect-error untyped lib
85 | ...typeCheckingConfigs,
86 | files: ['**/*.{ts,mts,cts,tsx}'],
87 | ignores: [
88 | // @ts-expect-error untyped lib
89 | ...(typeCheckingConfigs.ignores || []),
90 | integratedExamplesPath
91 | ]
92 | })
93 | }
94 |
95 | // Add our custom settings
96 | config.push({
97 | name: 'app/settings',
98 | files: ['**/*.{ts,mts,cts,tsx,js,mjs,cjs}'],
99 | ignores: [integratedExamplesPath],
100 | languageOptions: {
101 | parserOptions: {
102 | projectService: true,
103 | tsconfigRootDir: import.meta.dirname,
104 | },
105 | },
106 | plugins: {
107 | 'simple-import-sort': simpleImportSort,
108 | 'unused-imports': unusedImports
109 | },
110 | rules: {
111 | // https://github.com/lydell/eslint-plugin-simple-import-sort/#usage
112 | 'simple-import-sort/imports': 'error',
113 | 'simple-import-sort/exports': 'error',
114 | 'sort-imports': 'off',
115 | 'import/order': 'off',
116 |
117 | // https://github.com/sweepline/eslint-plugin-unused-imports
118 | '@typescript-eslint/no-unused-vars': 'off',
119 | 'unused-imports/no-unused-imports': 'error',
120 | 'unused-imports/no-unused-vars': ['error', { vars: 'all', args: 'none', caughtErrors: 'none' }],
121 |
122 | '@typescript-eslint/no-misused-promises': ['error', { checksVoidReturn: false }],
123 | },
124 | })
125 |
126 | export default config
127 |
--------------------------------------------------------------------------------
/examples/README.md:
--------------------------------------------------------------------------------
1 | Examples of use cases using this library
2 |
3 | Kept here for linting & typechecking purposes of the final use cases
4 |
--------------------------------------------------------------------------------
/examples/compress.ts:
--------------------------------------------------------------------------------
1 | /* eslint-disable unused-imports/no-unused-vars */
2 | import { compress, encode, findTextureMinMax } from '@monogrid/gainmap-js/encode'
3 | import { EXRLoader } from 'three/examples/jsm/loaders/EXRLoader.js'
4 |
5 | // load an HDR file
6 | const loader = new EXRLoader()
7 | const image = await loader.loadAsync('image.exr')
8 |
9 | // find RAW RGB Max value of a texture
10 | const textureMax = findTextureMinMax(image)
11 |
12 | // Encode the gainmap
13 | const encodingResult = encode({
14 | image,
15 | // this will encode the full HDR range
16 | maxContentBoost: Math.max.apply(this, textureMax)
17 | })
18 |
19 | // obtain the RAW RGBA SDR buffer and create an ImageData
20 | const sdrImageData = new ImageData(encodingResult.sdr.toArray(), encodingResult.sdr.width, encodingResult.sdr.height)
21 | // obtain the RAW RGBA Gain map buffer and create an ImageData
22 | const gainMapImageData = new ImageData(encodingResult.gainMap.toArray(), encodingResult.gainMap.width, encodingResult.gainMap.height)
23 |
24 | // parallel compress the RAW buffers into the specified mimeType
25 | const mimeType = 'image/jpeg'
26 | const quality = 0.9
27 | const [sdr, gainMap] = await Promise.all([
28 | compress({
29 | source: sdrImageData,
30 | mimeType,
31 | quality,
32 | flipY: true // output needs to be flipped
33 | }),
34 | compress({
35 | source: gainMapImageData,
36 | mimeType,
37 | quality,
38 | flipY: true // output needs to be flipped
39 | })
40 | ])
41 |
42 | // `sdr` will contain a JPEG which can be saved somewhere
43 | // `gainMap` will contain a JPEG which can be saved somewhere
44 |
45 | // renderers be manually disposed
46 | encodingResult.sdr.dispose()
47 | encodingResult.gainMap.dispose()
48 |
--------------------------------------------------------------------------------
/examples/decode-from-jpeg-using-loader.ts:
--------------------------------------------------------------------------------
1 | import { HDRJPGLoader } from '@monogrid/gainmap-js'
2 | import {
3 | EquirectangularReflectionMapping,
4 | Mesh,
5 | MeshBasicMaterial,
6 | PerspectiveCamera,
7 | PlaneGeometry,
8 | Scene,
9 | WebGLRenderer
10 | } from 'three'
11 |
12 | const renderer = new WebGLRenderer()
13 |
14 | const loader = new HDRJPGLoader(renderer)
15 |
16 | const result = await loader.loadAsync('gainmap.jpeg')
17 | // `result` can be used to populate a Texture
18 |
19 | const scene = new Scene()
20 | const mesh = new Mesh(
21 | new PlaneGeometry(),
22 | new MeshBasicMaterial({ map: result.renderTarget.texture })
23 | )
24 | scene.add(mesh)
25 | renderer.render(scene, new PerspectiveCamera())
26 |
27 | // Starting from three.js r159
28 | // `result.renderTarget.texture` can
29 | // also be used as Equirectangular scene background
30 | //
31 | // it was previously needed to convert it
32 | // to a DataTexture with `result.toDataTexture()`
33 | scene.background = result.renderTarget.texture
34 | scene.background.mapping = EquirectangularReflectionMapping
35 |
36 | // result must be manually disposed
37 | // when you are done using it
38 | result.dispose()
39 |
--------------------------------------------------------------------------------
/examples/decode-from-jpeg.ts:
--------------------------------------------------------------------------------
1 | import { decode, extractGainmapFromJPEG } from '@monogrid/gainmap-js'
2 | import {
3 | ClampToEdgeWrapping,
4 | LinearFilter,
5 | LinearMipMapLinearFilter,
6 | LinearSRGBColorSpace,
7 | Mesh,
8 | MeshBasicMaterial,
9 | PerspectiveCamera,
10 | PlaneGeometry,
11 | RGBAFormat,
12 | Scene,
13 | SRGBColorSpace,
14 | Texture,
15 | UnsignedByteType,
16 | UVMapping,
17 | WebGLRenderer
18 | } from 'three'
19 |
20 | const renderer = new WebGLRenderer()
21 |
22 | // fetch a JPEG image containing a gainmap as ArrayBuffer
23 | const jpeg = new Uint8Array(await (await fetch('gainmap.jpeg')).arrayBuffer())
24 |
25 | // extract data from the JPEG
26 | const { gainMap: gainMapBuffer, sdr: sdrBuffer, metadata } = await extractGainmapFromJPEG(jpeg)
27 |
28 | // create data blobs
29 | const gainMapBlob = new Blob([gainMapBuffer], { type: 'image/jpeg' })
30 | const sdrBlob = new Blob([sdrBuffer], { type: 'image/jpeg' })
31 |
32 | // create ImageBitmap data
33 | const [gainMapImageBitmap, sdrImageBitmap] = await Promise.all([
34 | createImageBitmap(gainMapBlob, { imageOrientation: 'flipY' }),
35 | createImageBitmap(sdrBlob, { imageOrientation: 'flipY' })
36 | ])
37 |
38 | // create textures
39 | const gainMap = new Texture(gainMapImageBitmap,
40 | UVMapping,
41 | ClampToEdgeWrapping,
42 | ClampToEdgeWrapping,
43 | LinearFilter,
44 | LinearMipMapLinearFilter,
45 | RGBAFormat,
46 | UnsignedByteType,
47 | 1,
48 | LinearSRGBColorSpace
49 | )
50 |
51 | gainMap.needsUpdate = true
52 |
53 | // create textures
54 | const sdr = new Texture(sdrImageBitmap,
55 | UVMapping,
56 | ClampToEdgeWrapping,
57 | ClampToEdgeWrapping,
58 | LinearFilter,
59 | LinearMipMapLinearFilter,
60 | RGBAFormat,
61 | UnsignedByteType,
62 | 1,
63 | SRGBColorSpace
64 | )
65 |
66 | sdr.needsUpdate = true
67 |
68 | // restore the HDR texture
69 | const result = decode({
70 | sdr,
71 | gainMap,
72 | // this allows to use `result.renderTarget.texture` directly
73 | renderer,
74 | // this will restore the full HDR range
75 | maxDisplayBoost: Math.pow(2, metadata.hdrCapacityMax),
76 | ...metadata
77 | })
78 |
79 | const scene = new Scene()
80 | // `result` can be used to populate a Texture
81 | const mesh = new Mesh(
82 | new PlaneGeometry(),
83 | new MeshBasicMaterial({ map: result.renderTarget.texture })
84 | )
85 | scene.add(mesh)
86 | renderer.render(scene, new PerspectiveCamera())
87 |
88 | // result must be manually disposed
89 | // when you are done using it
90 | result.dispose()
91 |
--------------------------------------------------------------------------------
/examples/decode-from-separate-data-using-loader.ts:
--------------------------------------------------------------------------------
1 | import { GainMapLoader } from '@monogrid/gainmap-js'
2 | import {
3 | EquirectangularReflectionMapping,
4 | Mesh,
5 | MeshBasicMaterial,
6 | PerspectiveCamera,
7 | PlaneGeometry,
8 | Scene,
9 | WebGLRenderer
10 | } from 'three'
11 |
12 | const renderer = new WebGLRenderer()
13 |
14 | const loader = new GainMapLoader(renderer)
15 |
16 | const result = await loader.loadAsync(['sdr.jpeg', 'gainmap.jpeg', 'metadata.json'])
17 | // `result` can be used to populate a Texture
18 |
19 | const scene = new Scene()
20 | const mesh = new Mesh(
21 | new PlaneGeometry(),
22 | new MeshBasicMaterial({ map: result.renderTarget.texture })
23 | )
24 | scene.add(mesh)
25 | renderer.render(scene, new PerspectiveCamera())
26 |
27 | // Starting from three.js r159
28 | // `result.renderTarget.texture` can
29 | // also be used as Equirectangular scene background
30 | //
31 | // it was previously needed to convert it
32 | // to a DataTexture with `result.toDataTexture()`
33 | scene.background = result.renderTarget.texture
34 | scene.background.mapping = EquirectangularReflectionMapping
35 |
36 | // result must be manually disposed
37 | // when you are done using it
38 | result.dispose()
39 |
--------------------------------------------------------------------------------
/examples/decode-from-separate-data.ts:
--------------------------------------------------------------------------------
1 | import { decode, GainMapMetadata } from '@monogrid/gainmap-js'
2 | import {
3 | Mesh,
4 | MeshBasicMaterial,
5 | PerspectiveCamera,
6 | PlaneGeometry,
7 | Scene,
8 | TextureLoader,
9 | WebGLRenderer
10 | } from 'three'
11 |
12 | const renderer = new WebGLRenderer()
13 |
14 | const textureLoader = new TextureLoader()
15 |
16 | // load SDR Representation
17 | const sdr = await textureLoader.loadAsync('sdr.jpg')
18 | // load Gain map recovery image
19 | const gainMap = await textureLoader.loadAsync('gainmap.jpg')
20 | // load metadata
21 | const metadata = await (await fetch('metadata.json')).json() as GainMapMetadata
22 |
23 | const result = decode({
24 | sdr,
25 | gainMap,
26 | // this allows to use `result.renderTarget.texture` directly
27 | renderer,
28 | // this will restore the full HDR range
29 | maxDisplayBoost: Math.pow(2, metadata.hdrCapacityMax),
30 | ...metadata
31 | })
32 |
33 | const scene = new Scene()
34 | // `result` can be used to populate a Texture
35 | const mesh = new Mesh(
36 | new PlaneGeometry(),
37 | new MeshBasicMaterial({ map: result.renderTarget.texture })
38 | )
39 | scene.add(mesh)
40 | renderer.render(scene, new PerspectiveCamera())
41 |
42 | // result must be manually disposed
43 | // when you are done using it
44 | result.dispose()
45 |
--------------------------------------------------------------------------------
/examples/encode-and-compress.ts:
--------------------------------------------------------------------------------
1 | /* eslint-disable unused-imports/no-unused-vars */
2 | import { encodeAndCompress, findTextureMinMax } from '@monogrid/gainmap-js/encode'
3 | import { encodeJPEGMetadata } from '@monogrid/gainmap-js/libultrahdr'
4 | import { EXRLoader } from 'three/examples/jsm/loaders/EXRLoader.js'
5 |
6 | // load an HDR file
7 | const loader = new EXRLoader()
8 | const image = await loader.loadAsync('image.exr')
9 |
10 | // find RAW RGB Max value of a texture
11 | const textureMax = findTextureMinMax(image)
12 |
13 | // Encode the gainmap
14 | const encodingResult = await encodeAndCompress({
15 | image,
16 | // this will encode the full HDR range
17 | maxContentBoost: Math.max.apply(this, textureMax),
18 | mimeType: 'image/jpeg'
19 | })
20 |
21 | // embed the compressed images + metadata into a single
22 | // JPEG file
23 | const jpeg = await encodeJPEGMetadata({
24 | ...encodingResult,
25 | sdr: encodingResult.sdr,
26 | gainMap: encodingResult.gainMap
27 | })
28 |
29 | // `jpeg` will be an `Uint8Array` which can be saved somewhere
30 |
--------------------------------------------------------------------------------
/examples/encode-jpeg-metadata.ts:
--------------------------------------------------------------------------------
1 | /* eslint-disable unused-imports/no-unused-vars */
2 | import { compress, encode, findTextureMinMax } from '@monogrid/gainmap-js/encode'
3 | import { encodeJPEGMetadata } from '@monogrid/gainmap-js/libultrahdr'
4 | import { EXRLoader } from 'three/examples/jsm/loaders/EXRLoader.js'
5 |
6 | // load an HDR file
7 | const loader = new EXRLoader()
8 | const image = await loader.loadAsync('image.exr')
9 |
10 | // find RAW RGB Max value of a texture
11 | const textureMax = findTextureMinMax(image)
12 |
13 | // Encode the gainmap
14 | const encodingResult = encode({
15 | image,
16 | // this will encode the full HDR range
17 | maxContentBoost: Math.max.apply(this, textureMax)
18 | })
19 |
20 | // obtain the RAW RGBA SDR buffer and create an ImageData
21 | const sdrImageData = new ImageData(encodingResult.sdr.toArray(), encodingResult.sdr.width, encodingResult.sdr.height)
22 | // obtain the RAW RGBA Gain map buffer and create an ImageData
23 | const gainMapImageData = new ImageData(encodingResult.gainMap.toArray(), encodingResult.gainMap.width, encodingResult.gainMap.height)
24 |
25 | // parallel compress the RAW buffers into the specified mimeType
26 | const mimeType = 'image/jpeg'
27 | const quality = 0.9
28 |
29 | const [sdr, gainMap] = await Promise.all([
30 | compress({
31 | source: sdrImageData,
32 | mimeType,
33 | quality,
34 | flipY: true // output needs to be flipped
35 | }),
36 | compress({
37 | source: gainMapImageData,
38 | mimeType,
39 | quality,
40 | flipY: true // output needs to be flipped
41 | })
42 | ])
43 |
44 | // obtain the metadata which will be embedded into
45 | // and XMP tag inside the final JPEG file
46 | const metadata = encodingResult.getMetadata()
47 |
48 | // embed the compressed images + metadata into a single
49 | // JPEG file
50 | const jpeg = await encodeJPEGMetadata({
51 | ...encodingResult,
52 | ...metadata,
53 | sdr,
54 | gainMap
55 | })
56 |
57 | // `jpeg` will be an `Uint8Array` which can be saved somewhere
58 |
59 | // encoder must be manually disposed
60 | // when no longer needed
61 | encodingResult.gainMap.dispose()
62 | encodingResult.sdr.dispose()
63 |
--------------------------------------------------------------------------------
/examples/encode.ts:
--------------------------------------------------------------------------------
1 | import { encode, findTextureMinMax } from '@monogrid/gainmap-js/encode'
2 | import { EXRLoader } from 'three/examples/jsm/loaders/EXRLoader.js'
3 |
4 | // load an HDR file
5 | const loader = new EXRLoader()
6 | const image = await loader.loadAsync('image.exr')
7 |
8 | // find RAW RGB Max value of a texture
9 | const textureMax = findTextureMinMax(image)
10 |
11 | // Encode the gainmap
12 | const encodingResult = encode({
13 | image,
14 | // this will encode the full HDR range
15 | maxContentBoost: Math.max.apply(this, textureMax)
16 | })
17 |
18 | // can be re-encoded after changing parameters
19 | encodingResult.sdr.material.exposure = 0.9
20 | encodingResult.sdr.render()
21 | // or
22 | encodingResult.gainMap.material.gamma = [1.1, 1.1, 1.1]
23 | encodingResult.gainMap.render()
24 |
25 | // do something with encodingResult.gainMap.toArray()
26 | // and encodingResult.sdr.toArray()
27 |
28 | // renderers be manually disposed
29 | encodingResult.sdr.dispose()
30 | encodingResult.gainMap.dispose()
31 |
--------------------------------------------------------------------------------
/examples/globals.d.ts:
--------------------------------------------------------------------------------
1 | declare module '*?worker' {
2 | const workerConstructor: {
3 | new (): Worker
4 | }
5 | export default workerConstructor
6 | }
7 |
--------------------------------------------------------------------------------
/examples/integrated/.editorconfig:
--------------------------------------------------------------------------------
1 | root = true
2 |
3 | [*]
4 | charset = utf-8
5 | indent_style = tab
6 | indent_size = 2
7 | end_of_line = lf
8 | insert_final_newline = true
9 | trim_trailing_whitespace = true
10 |
--------------------------------------------------------------------------------
/examples/integrated/README.md:
--------------------------------------------------------------------------------
1 | Examples of integrated html pages using both three.js and this library
2 |
--------------------------------------------------------------------------------
/examples/integrated/jstest.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
12 |
144 |
145 |
146 |
147 |
148 |
149 |
--------------------------------------------------------------------------------
/examples/integrated/libs/basis/README.md:
--------------------------------------------------------------------------------
1 | # Basis Universal GPU Texture Compression
2 |
3 | Basis Universal is a "[supercompressed](http://gamma.cs.unc.edu/GST/gst.pdf)"
4 | GPU texture and texture video compression system that outputs a highly
5 | compressed intermediate file format (.basis) that can be quickly transcoded to
6 | a wide variety of GPU texture compression formats.
7 |
8 | [GitHub](https://github.com/BinomialLLC/basis_universal)
9 |
10 | ## Transcoders
11 |
12 | Basis Universal texture data may be used in two different file formats:
13 | `.basis` and `.ktx2`, where `ktx2` is a standardized wrapper around basis texture data.
14 |
15 | For further documentation about the Basis compressor and transcoder, refer to
16 | the [Basis GitHub repository](https://github.com/BinomialLLC/basis_universal).
17 |
18 | The folder contains two files required for transcoding `.basis` or `.ktx2` textures:
19 |
20 | * `basis_transcoder.js` — JavaScript wrapper for the WebAssembly transcoder.
21 | * `basis_transcoder.wasm` — WebAssembly transcoder.
22 |
23 | Both are dependencies of `KTX2Loader`:
24 |
25 | ```js
26 | const ktx2Loader = new KTX2Loader();
27 | ktx2Loader.setTranscoderPath( 'examples/jsm/libs/basis/' );
28 | ktx2Loader.detectSupport( renderer );
29 | ktx2Loader.load( 'diffuse.ktx2', function ( texture ) {
30 |
31 | const material = new THREE.MeshStandardMaterial( { map: texture } );
32 |
33 | }, function () {
34 |
35 | console.log( 'onProgress' );
36 |
37 | }, function ( e ) {
38 |
39 | console.error( e );
40 |
41 | } );
42 | ```
43 |
44 | ## License
45 |
46 | [Apache License 2.0](https://github.com/BinomialLLC/basis_universal/blob/master/LICENSE)
47 |
--------------------------------------------------------------------------------
/examples/integrated/libs/basis/basis_transcoder.wasm:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/MONOGRID/gainmap-js/fe719b9781d867b5f9487c0a13ba8eea6fac0335/examples/integrated/libs/basis/basis_transcoder.wasm
--------------------------------------------------------------------------------
/examples/integrated/main.css:
--------------------------------------------------------------------------------
1 | body {
2 | margin: 0;
3 | background-color: #000;
4 | color: #fff;
5 | font-family: Monospace;
6 | font-size: 13px;
7 | line-height: 24px;
8 | overscroll-behavior: none;
9 | }
10 |
11 | a {
12 | color: #ff0;
13 | text-decoration: none;
14 | }
15 |
16 | a:hover {
17 | text-decoration: underline;
18 | }
19 |
20 | button {
21 | cursor: pointer;
22 | text-transform: uppercase;
23 | }
24 |
25 | #info {
26 | position: absolute;
27 | top: 0px;
28 | width: 100%;
29 | padding: 10px;
30 | box-sizing: border-box;
31 | text-align: center;
32 | -moz-user-select: none;
33 | -webkit-user-select: none;
34 | -ms-user-select: none;
35 | user-select: none;
36 | pointer-events: none;
37 | z-index: 1; /* TODO Solve this in HTML */
38 | }
39 |
40 | a, button, input, select {
41 | pointer-events: auto;
42 | }
43 |
44 | .lil-gui {
45 | z-index: 2 !important; /* TODO Solve this in HTML */
46 | }
47 |
48 | @media all and ( max-width: 640px ) {
49 | .lil-gui.root {
50 | right: auto;
51 | top: auto;
52 | max-height: 50%;
53 | max-width: 80%;
54 | bottom: 0;
55 | left: 0;
56 | }
57 | }
58 |
59 | #overlay {
60 | position: absolute;
61 | font-size: 16px;
62 | z-index: 2;
63 | top: 0;
64 | left: 0;
65 | width: 100%;
66 | height: 100%;
67 | display: flex;
68 | align-items: center;
69 | justify-content: center;
70 | flex-direction: column;
71 | background: rgba(0,0,0,0.7);
72 | }
73 |
74 | #overlay button {
75 | background: transparent;
76 | border: 0;
77 | border: 1px solid rgb(255, 255, 255);
78 | border-radius: 4px;
79 | color: #ffffff;
80 | padding: 12px 18px;
81 | text-transform: uppercase;
82 | cursor: pointer;
83 | }
84 |
85 | #notSupported {
86 | width: 50%;
87 | margin: auto;
88 | background-color: #f00;
89 | margin-top: 20px;
90 | padding: 10px;
91 | }
92 |
--------------------------------------------------------------------------------
/examples/integrated/textures/gainmap/qwantani_puresky_8k-gainmap.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/MONOGRID/gainmap-js/fe719b9781d867b5f9487c0a13ba8eea6fac0335/examples/integrated/textures/gainmap/qwantani_puresky_8k-gainmap.jpg
--------------------------------------------------------------------------------
/examples/integrated/textures/gainmap/qwantani_puresky_8k-gainmap.ktx2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/MONOGRID/gainmap-js/fe719b9781d867b5f9487c0a13ba8eea6fac0335/examples/integrated/textures/gainmap/qwantani_puresky_8k-gainmap.ktx2
--------------------------------------------------------------------------------
/examples/integrated/textures/gainmap/qwantani_puresky_8k.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/MONOGRID/gainmap-js/fe719b9781d867b5f9487c0a13ba8eea6fac0335/examples/integrated/textures/gainmap/qwantani_puresky_8k.jpg
--------------------------------------------------------------------------------
/examples/integrated/textures/gainmap/qwantani_puresky_8k.json:
--------------------------------------------------------------------------------
1 | {
2 | "gainMapMax": [
3 | 15.868822554774999,
4 | 15.868822554774999,
5 | 15.868822554774999
6 | ],
7 | "gainMapMin": [
8 | 0,
9 | 0,
10 | 0
11 | ],
12 | "gamma": [
13 | 1,
14 | 1,
15 | 1
16 | ],
17 | "hdrCapacityMax": 15.868822554774999,
18 | "hdrCapacityMin": 0,
19 | "offsetHdr": [
20 | 0.015625,
21 | 0.015625,
22 | 0.015625
23 | ],
24 | "offsetSdr": [
25 | 0.015625,
26 | 0.015625,
27 | 0.015625
28 | ]
29 | }
--------------------------------------------------------------------------------
/examples/integrated/textures/gainmap/qwantani_puresky_8k.ktx2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/MONOGRID/gainmap-js/fe719b9781d867b5f9487c0a13ba8eea6fac0335/examples/integrated/textures/gainmap/qwantani_puresky_8k.ktx2
--------------------------------------------------------------------------------
/examples/integrated/textures/gainmap/spruit_sunrise_1k.hdr:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/MONOGRID/gainmap-js/fe719b9781d867b5f9487c0a13ba8eea6fac0335/examples/integrated/textures/gainmap/spruit_sunrise_1k.hdr
--------------------------------------------------------------------------------
/examples/integrated/textures/gainmap/spruit_sunrise_4k-gainmap.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/MONOGRID/gainmap-js/fe719b9781d867b5f9487c0a13ba8eea6fac0335/examples/integrated/textures/gainmap/spruit_sunrise_4k-gainmap.webp
--------------------------------------------------------------------------------
/examples/integrated/textures/gainmap/spruit_sunrise_4k.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/MONOGRID/gainmap-js/fe719b9781d867b5f9487c0a13ba8eea6fac0335/examples/integrated/textures/gainmap/spruit_sunrise_4k.jpg
--------------------------------------------------------------------------------
/examples/integrated/textures/gainmap/spruit_sunrise_4k.json:
--------------------------------------------------------------------------------
1 | {
2 | "gainMapMax": [
3 | 15.99929538702341,
4 | 15.99929538702341,
5 | 15.99929538702341
6 | ],
7 | "gainMapMin": [
8 | 0,
9 | 0,
10 | 0
11 | ],
12 | "gamma": [
13 | 1,
14 | 1,
15 | 1
16 | ],
17 | "hdrCapacityMax": 15.99929538702341,
18 | "hdrCapacityMin": 0,
19 | "offsetHdr": [
20 | 0.015625,
21 | 0.015625,
22 | 0.015625
23 | ],
24 | "offsetSdr": [
25 | 0.015625,
26 | 0.015625,
27 | 0.015625
28 | ]
29 | }
--------------------------------------------------------------------------------
/examples/integrated/textures/gainmap/spruit_sunrise_4k.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/MONOGRID/gainmap-js/fe719b9781d867b5f9487c0a13ba8eea6fac0335/examples/integrated/textures/gainmap/spruit_sunrise_4k.webp
--------------------------------------------------------------------------------
/examples/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "strict": true,
4 | "strictNullChecks": true,
5 | "strictFunctionTypes": true,
6 | "noErrorTruncation": true,
7 | "noImplicitAny": true,
8 | "noImplicitThis": true,
9 | "module": "ES2022",
10 | "target": "ES2018",
11 | "moduleResolution": "Bundler",
12 | "resolvePackageJsonExports": true,
13 | "resolvePackageJsonImports": true,
14 | "allowJs": true,
15 | "checkJs": true,
16 | "noEmit": true,
17 | "skipLibCheck": true,
18 | "lib": ["ES2022", "DOM"],
19 | "paths": {
20 | "@monogrid/gainmap-js": ["../dist"],
21 | "@monogrid/gainmap-js/libultrahdr": ["../dist/libultrahdr"],
22 | "@monogrid/gainmap-js/worker": ["../dist/worker"],
23 | "@monogrid/gainmap-js/worker-interface": ["../dist/worker-interface"]
24 | },
25 | },
26 | "exclude": ["./integrated/libs"]
27 | }
28 |
--------------------------------------------------------------------------------
/examples/worker.ts:
--------------------------------------------------------------------------------
1 | /* eslint-disable unused-imports/no-unused-vars */
2 | import { encodeAndCompress, findTextureMinMax } from '@monogrid/gainmap-js/encode'
3 | import { encodeJPEGMetadata } from '@monogrid/gainmap-js/libultrahdr'
4 | // this assumes a vite-like bundler understands the `?worker` import
5 | import GainMapWorker from '@monogrid/gainmap-js/worker?worker'
6 | import { getPromiseWorker, getWorkerInterface } from '@monogrid/gainmap-js/worker-interface'
7 | import { EXRLoader } from 'three/examples/jsm/loaders/EXRLoader.js'
8 |
9 | // turn our Worker into a PromiseWorker
10 | const promiseWorker = getPromiseWorker(new GainMapWorker())
11 | // get the interface
12 | const workerInterface = getWorkerInterface(promiseWorker)
13 |
14 | // load an HDR file
15 | const loader = new EXRLoader()
16 | const image = await loader.loadAsync('image.exr')
17 |
18 | // find RAW RGB Max value of a texture
19 | const textureMax = findTextureMinMax(image)
20 |
21 | // Encode the gainmap
22 | const encodingResult = await encodeAndCompress({
23 | image,
24 | // this will encode the full HDR range
25 | maxContentBoost: Math.max.apply(this, textureMax),
26 | // use our worker for compressing the image
27 | withWorker: workerInterface,
28 | mimeType: 'image/jpeg'
29 | })
30 |
31 | // embed the compressed images + metadata into a single
32 | // JPEG file
33 | const jpeg = await encodeJPEGMetadata({
34 | ...encodingResult,
35 | sdr: encodingResult.sdr,
36 | gainMap: encodingResult.gainMap
37 | })
38 |
39 | // `jpeg` will be an `Uint8Array` which can be saved somewhere
40 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@monogrid/gainmap-js",
3 | "version": "3.1.0",
4 | "description": "A Javascript (TypeScript) Port of Adobe Gainmap Technology for storing HDR Images using an SDR Image + a gain map",
5 | "keywords": [
6 | "hdr",
7 | "gain map",
8 | "gainmap",
9 | "three",
10 | "threejs"
11 | ],
12 | "homepage": "https://github.com/MONOGRID/gainmap-js#readme",
13 | "bugs": {
14 | "url": "https://github.com/MONOGRID/gainmap-js/issues"
15 | },
16 | "repository": {
17 | "type": "git",
18 | "url": "git+ssh://git@github.com/MONOGRID/gainmap-js.git"
19 | },
20 | "license": "MIT",
21 | "author": "MONOGRID ",
22 | "sideEffects": false,
23 | "type": "module",
24 | "exports": {
25 | ".": {
26 | "types": "./dist/decode.d.ts",
27 | "import": "./dist/decode.js",
28 | "default": "./dist/decode.umd.cjs"
29 | },
30 | "./encode": {
31 | "types": "./dist/encode.d.ts",
32 | "import": "./dist/encode.js",
33 | "default": "./dist/encode.umd.cjs"
34 | },
35 | "./libultrahdr": {
36 | "types": "./dist/libultrahdr.d.ts",
37 | "import": "./dist/libultrahdr.js",
38 | "default": "./dist/libultrahdr.umd.cjs"
39 | },
40 | "./worker": {
41 | "types": "./dist/worker.d.ts",
42 | "import": "./dist/worker.js",
43 | "default": "./dist/worker.umd.cjs"
44 | },
45 | "./worker-interface": {
46 | "types": "./dist/worker-interface.d.ts",
47 | "import": "./dist/worker-interface.js",
48 | "default": "./dist/worker-interface.umd.cjs"
49 | }
50 | },
51 | "main": "dist/decode.umd.cjs",
52 | "module": "dist/decode.js",
53 | "types": "dist/decode.d.ts",
54 | "typesVersions": {
55 | "*": {
56 | ".": [
57 | "./dist/decode.d.ts"
58 | ],
59 | "encode": [
60 | "./dist/encode.d.ts"
61 | ],
62 | "libultrahdr": [
63 | "./dist/libultrahdr.d.ts"
64 | ],
65 | "worker": [
66 | "./dist/worker.d.ts"
67 | ],
68 | "worker-interface": [
69 | "./dist/worker-interface.d.ts"
70 | ]
71 | }
72 | },
73 | "scripts": {
74 | "build": "rollup -c",
75 | "check": "concurrently -c auto npm:check:*",
76 | "check:eslint-examples": "eslint \"examples/**/*.{ts,html}\"",
77 | "check:eslint-src": "eslint \"src/**/*.ts\"",
78 | "check:eslint-tests": "eslint \"tests/**/*.ts\"",
79 | "check:typecheck-examples": "tsc -p examples",
80 | "check:typecheck-src": "tsc -p src",
81 | "check:typecheck-tests": "tsc -p tests",
82 | "ci:check": "concurrently npm:ci:check:*",
83 | "ci:check:eslint-examples": "eslint --format json --output-file reports/eslint-examples.json \"examples/**/*.{ts,html}\"",
84 | "ci:check:eslint-src": "eslint --format json --output-file reports/eslint-src.json \"src/**/*.ts\"",
85 | "ci:check:eslint-tests": "eslint --format json --output-file reports/eslint-tests.json \"tests/**/*.ts\"",
86 | "ci:check:typecheck-examples": "tsc --pretty false -p examples > reports/typecheck-examples.log",
87 | "ci:check:typecheck-src": "tsc --pretty false -p src > reports/typecheck.log",
88 | "ci:check:typecheck-tests": "tsc --pretty false -p tests > reports/typecheck-tests.log",
89 | "dev": "concurrently -n rollup,servez -c magenta,green \"rollup -c -w\" \"servez\"",
90 | "prepack": "npm run build",
91 | "start": "concurrently -n rollup,servez -c magenta,green \"rollup -c -w\" \"servez\"",
92 | "test": "nyc --reporter=text --reporter=lcov playwright test",
93 | "test:codegen": "playwright codegen http://localhost:8080",
94 | "test:docker": "docker run --rm --network host -v $(pwd):/work -w /work -it -u $(id -u ${USER}):$(id -g ${USER}) mcr.microsoft.com/playwright:v1.51.1-jammy /bin/bash",
95 | "test:startserver": "rollup -c && servez"
96 | },
97 | "config": {
98 | "commitizen": {
99 | "path": "./node_modules/cz-conventional-changelog"
100 | }
101 | },
102 | "browserslist": [
103 | "> 1%, not dead, not ie 11, not op_mini all"
104 | ],
105 | "dependencies": {
106 | "promise-worker-transferable": "^1.0.4"
107 | },
108 | "devDependencies": {
109 | "@eslint/eslintrc": "^3.3.1",
110 | "@playwright/test": "^1.51.1",
111 | "@rollup/plugin-commonjs": "^26.0.1",
112 | "@rollup/plugin-json": "^6.1.0",
113 | "@rollup/plugin-node-resolve": "^15.2.3",
114 | "@rollup/plugin-terser": "^0.4.4",
115 | "@rollup/plugin-typescript": "^11.1.6",
116 | "@semantic-release/changelog": "^6.0.3",
117 | "@semantic-release/commit-analyzer": "^13.0.0",
118 | "@semantic-release/git": "github:semantic-release/git",
119 | "@semantic-release/github": "^10.1.3",
120 | "@semantic-release/npm": "^12.0.1",
121 | "@semantic-release/release-notes-generator": "^14.0.1",
122 | "@types/node": "^20.14.9",
123 | "@types/three": "^0.175.0",
124 | "concurrently": "^8.2.2",
125 | "conventional-changelog-conventionalcommits": "^8.0.0",
126 | "cz-conventional-changelog": "^3.3.0",
127 | "eslint": "^9.16.0",
128 | "eslint-config-mdcs": "^5.0.0",
129 | "eslint-plugin-html": "^8.0.0",
130 | "eslint-plugin-simple-import-sort": "^12.1.1",
131 | "eslint-plugin-unused-imports": "^4.1.4",
132 | "globals": "^16.0.0",
133 | "neostandard": "^0.12.1",
134 | "nyc": "^17.0.0",
135 | "rollup": "^4.20.0",
136 | "rollup-plugin-copy": "^3.5.0",
137 | "rollup-plugin-delete": "^2.0.0",
138 | "rollup-plugin-istanbul": "^5.0.0",
139 | "rollup-plugin-license": "^3.5.2",
140 | "semantic-release": "^24.0.0",
141 | "servez": "^2.2.3",
142 | "sharp": "^0.33.4",
143 | "three": "^0.175.0",
144 | "typedoc": "^0.28.0",
145 | "typedoc-github-wiki-theme": "^2.1.0",
146 | "typedoc-plugin-markdown": "^4.2.3",
147 | "typescript": "^5.5.4"
148 | },
149 | "peerDependencies": {
150 | "three": ">= 0.159.0"
151 | },
152 | "publishConfig": {
153 | "access": "public",
154 | "registry": "https://registry.npmjs.org/"
155 | }
156 | }
157 |
--------------------------------------------------------------------------------
/playwright.config.ts:
--------------------------------------------------------------------------------
1 | import { defineConfig, devices } from '@playwright/test'
2 |
3 | /**
4 | * Read environment variables from file.
5 | * https://github.com/motdotla/dotenv
6 | */
7 | // require('dotenv').config();
8 |
9 | /**
10 | * See https://playwright.dev/docs/test-configuration.
11 | */
12 | export default defineConfig({
13 | testDir: './tests',
14 | /* Run tests in files in parallel */
15 | fullyParallel: true,
16 | /* Fail the build on CI if you accidentally left test.only in the source code. */
17 | forbidOnly: !!process.env.CI,
18 | /* Retry on CI only */
19 | retries: process.env.CI ? 3 : 0,
20 |
21 | /* Opt out of parallel tests on CI. */
22 | workers: process.env.CI ? 1 : undefined,
23 | /* Reporter to use. See https://playwright.dev/docs/test-reporters */
24 | reporter: [
25 | ['html'],
26 | ['json', { outputFile: 'playwright-report.json' }]
27 | ],
28 | /* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */
29 | use: {
30 | /* Base URL to use in actions like `await page.goto('/')`. */
31 | baseURL: 'http://127.0.0.1:8080',
32 |
33 | /* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */
34 | trace: process.env.CI ? 'on-first-retry' : 'retain-on-failure',
35 |
36 | // Emulates the user locale.
37 | locale: 'en-GB',
38 |
39 | // Emulates the user timezone.
40 | timezoneId: 'Europe/Paris'
41 | },
42 |
43 | // expect timeout to 40 seconds
44 | expect: {
45 | timeout: 40 * 1000
46 | },
47 |
48 | // test timeout to 60 seconds
49 | timeout: 60 * 1000,
50 |
51 | // https://github.com/microsoft/playwright/issues/7575#issuecomment-1693400652
52 | // same screenshot name across platforms
53 | snapshotPathTemplate: 'tests/__snapshots__/{testFilePath}/{projectName}-{arg}{ext}',
54 |
55 | /* Configure projects for major browsers */
56 | projects: [
57 | {
58 | name: 'chromium',
59 | use: { ...devices['Desktop Chrome'], launchOptions: { args: ['--font-render-hinting=none'] }, viewport: { width: 500, height: 500 } }
60 | }
61 |
62 | // {
63 | // name: 'firefox',
64 | // use: { ...devices['Desktop Firefox'], launchOptions: { args: ['--font-render-hinting=none'] } },
65 | // testIgnore: ['**/panolens/**'] // FIXME: these tests don't work in this browser
66 | // },
67 |
68 | // {
69 | // name: 'webkit',
70 | // use: { ...devices['Desktop Safari'] },
71 | // testIgnore: ['**/panolens/**'] // FIXME: these tests don't work in this browser
72 | // }
73 |
74 | /* Test against mobile viewports. */
75 | // {
76 | // name: 'Mobile Chrome',
77 | // use: { ...devices['Pixel 5'] },
78 | // },
79 | // {
80 | // name: 'Mobile Safari',
81 | // use: { ...devices['iPhone 12'] },
82 | // },
83 |
84 | /* Test against branded browsers. */
85 | // {
86 | // name: 'Microsoft Edge',
87 | // use: { ...devices['Desktop Edge'], channel: 'msedge' },
88 | // },
89 | // {
90 | // name: 'Google Chrome',
91 | // use: { ...devices['Desktop Chrome'], channel: 'chrome' },
92 | // },
93 | ],
94 |
95 | /* Run your local dev server before starting the tests */
96 | webServer: {
97 | command: 'npm run test:startserver',
98 | url: 'http://127.0.0.1:8080',
99 | reuseExistingServer: !process.env.CI,
100 | env: {
101 | PLAYWRIGHT_TESTING: 'true'
102 | }
103 | }
104 | })
105 |
--------------------------------------------------------------------------------
/release.config.cjs:
--------------------------------------------------------------------------------
1 | /** @type {import('semantic-release').Options} */
2 | module.exports = {
3 | branches: ['main'],
4 | plugins: [
5 | ['@semantic-release/commit-analyzer', {
6 | preset: 'conventionalcommits',
7 | releaseRules: [
8 | { breaking: true, release: 'major' },
9 | { type: 'deps', release: 'patch' }
10 | ],
11 | parserOpts: {
12 | noteKeywords: ['BREAKING CHANGE', 'BREAKING CHANGES']
13 | }
14 | }],
15 | '@semantic-release/release-notes-generator',
16 | '@semantic-release/npm',
17 | '@semantic-release/github',
18 | ['@semantic-release/changelog', { changelogFile: 'CHANGELOG.md' }],
19 | ['@semantic-release/git', { assets: ['package.json', 'CHANGELOG.md'] }]
20 | ]
21 | }
22 |
--------------------------------------------------------------------------------
/reports/.gitkeep:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/MONOGRID/gainmap-js/fe719b9781d867b5f9487c0a13ba8eea6fac0335/reports/.gitkeep
--------------------------------------------------------------------------------
/rollup.config.decodeonly.mjs:
--------------------------------------------------------------------------------
1 | import commonjs from '@rollup/plugin-commonjs'
2 | import json from '@rollup/plugin-json'
3 | import resolve from '@rollup/plugin-node-resolve'
4 | import typescript from '@rollup/plugin-typescript'
5 | import { defineConfig } from 'rollup'
6 | import del from 'rollup-plugin-delete'
7 | // @ts-expect-error untyped library
8 | import istanbul from 'rollup-plugin-istanbul'
9 | import license from 'rollup-plugin-license'
10 |
11 | // @ts-expect-error tsc + rollup fight each other
12 | import pkgJSON from './package.json' assert { type: 'json' }
13 |
14 | const { author, name, version } = pkgJSON
15 |
16 | /** @type {import('rollup').OutputOptions} */
17 | const settings = {
18 | globals: {
19 | three: 'three'
20 | },
21 | sourcemap: !!process.env.PLAYWRIGHT_TESTING
22 | }
23 |
24 | const configBase = defineConfig({
25 | external: ['three']
26 | })
27 |
28 | /** @type {import('rollup').InputPluginOption[]} */
29 | const plugins = [
30 | json(),
31 | typescript({
32 | tsconfig: 'src/tsconfig.json',
33 | declaration: true,
34 | sourceMap: !!process.env.PLAYWRIGHT_TESTING,
35 | declarationDir: 'dist',
36 | include: ['src/**/*.ts'],
37 | exclude: ['src/libultrahdr.ts', 'src/libultrahdr/**/*.ts', 'src/encode.ts', 'src/encode/**/*.ts', 'src/worker*.ts']
38 | }),
39 | resolve(),
40 | commonjs({
41 | include: 'node_modules/**',
42 | extensions: ['.js'],
43 | ignoreGlobal: false,
44 | sourceMap: !!process.env.PLAYWRIGHT_TESTING
45 | }),
46 | license({
47 | banner: `
48 | ${name} v${version}
49 | With ❤️, by ${author}
50 | `
51 | })
52 | ]
53 |
54 | if (process.env.PLAYWRIGHT_TESTING) {
55 | plugins.push(
56 | istanbul({
57 | include: ['src/**/*.ts']
58 |
59 | })
60 | )
61 | }
62 |
63 | /** @type {import('rollup').RollupOptions[]} */
64 | let configs = [
65 | defineConfig({
66 | input: {
67 | decode: './src/decode.ts'
68 | },
69 | output: {
70 | dir: 'dist',
71 | name,
72 | format: 'es',
73 | ...settings
74 | },
75 | plugins: [
76 | del({ targets: 'dist/*' }),
77 | ...plugins
78 | ],
79 | ...configBase
80 | })
81 | ]
82 |
83 | // configs to produce when not testing
84 | // with playwright
85 | if (!process.env.PLAYWRIGHT_TESTING) {
86 | configs = configs.concat([
87 | // decode UMD
88 | defineConfig({
89 | input: './src/decode.ts',
90 | output: {
91 | format: 'umd',
92 | name,
93 | file: 'dist/decode.umd.js',
94 | ...settings
95 | },
96 | plugins,
97 | ...configBase
98 | })
99 | ])
100 | }
101 |
102 | export default configs
103 |
--------------------------------------------------------------------------------
/rollup.config.mjs:
--------------------------------------------------------------------------------
1 | import commonjs from '@rollup/plugin-commonjs'
2 | import json from '@rollup/plugin-json'
3 | import resolve from '@rollup/plugin-node-resolve'
4 | import typescript from '@rollup/plugin-typescript'
5 | import { defineConfig } from 'rollup'
6 | import copy from 'rollup-plugin-copy'
7 | import del from 'rollup-plugin-delete'
8 | // @ts-expect-error untyped library
9 | import istanbul from 'rollup-plugin-istanbul'
10 | import license from 'rollup-plugin-license'
11 |
12 | // @ts-expect-error tsc + rollup fight each other
13 | import pkgJSON from './package.json' assert { type: 'json' }
14 |
15 | const { author, name, version } = pkgJSON
16 |
17 | /** @type {import('rollup').OutputOptions} */
18 | const settings = {
19 | globals: {
20 | three: 'three'
21 | },
22 | sourcemap: !!process.env.PLAYWRIGHT_TESTING
23 | }
24 |
25 | const configBase = defineConfig({
26 | external: ['three']
27 | })
28 |
29 | /** @type {import('rollup').InputPluginOption[]} */
30 | const plugins = [
31 | json(),
32 | typescript({
33 | tsconfig: 'src/tsconfig.json',
34 | declaration: true,
35 | sourceMap: !!process.env.PLAYWRIGHT_TESTING,
36 | declarationDir: 'dist',
37 | include: ['src/**/*.ts']
38 | }),
39 | resolve(),
40 | commonjs({
41 | include: 'node_modules/**',
42 | extensions: ['.js'],
43 | ignoreGlobal: false,
44 | sourceMap: !!process.env.PLAYWRIGHT_TESTING
45 | }),
46 | license({
47 | banner: `
48 | ${name} v${version}
49 | With ❤️, by ${author}
50 | `
51 | })
52 | ]
53 |
54 | if (process.env.PLAYWRIGHT_TESTING) {
55 | plugins.push(
56 | istanbul({
57 | include: ['src/**/*.ts']
58 |
59 | })
60 | )
61 | }
62 |
63 | /** @type {import('rollup').RollupOptions[]} */
64 | let configs = [
65 | defineConfig({
66 | input: {
67 | encode: './src/encode.ts',
68 | decode: './src/decode.ts',
69 | libultrahdr: './src/libultrahdr.ts',
70 | worker: './src/worker.ts',
71 | 'worker-interface': './src/worker-interface.ts'
72 | },
73 | output: {
74 | dir: 'dist',
75 | name,
76 | format: 'es',
77 | ...settings
78 | },
79 | plugins: [
80 | del({ targets: 'dist/*' }),
81 | copy({
82 | targets: [
83 | { src: 'libultrahdr-wasm/build/libultrahdr-esm.wasm', dest: 'dist' }
84 | ]
85 | }),
86 | ...plugins
87 | ],
88 | ...configBase
89 | }),
90 |
91 | // worker UMD
92 | defineConfig({
93 | input: './src/worker.ts',
94 | output: {
95 | format: 'umd',
96 | name: 'worker',
97 | file: 'dist/worker.umd.cjs',
98 | ...settings
99 | },
100 | plugins,
101 | ...configBase
102 | })
103 | ]
104 |
105 | // configs to produce when not testing
106 | // with playwright
107 | if (!process.env.PLAYWRIGHT_TESTING) {
108 | configs = configs.concat([
109 |
110 | // decode UMD
111 | defineConfig({
112 | input: './src/decode.ts',
113 | output: {
114 | format: 'umd',
115 | name,
116 | file: 'dist/decode.umd.cjs',
117 | ...settings
118 | },
119 | plugins,
120 | ...configBase
121 | }),
122 |
123 | // encode UMD
124 | defineConfig({
125 | input: './src/encode.ts',
126 | output: {
127 | format: 'umd',
128 | name: 'encode',
129 | file: 'dist/encode.umd.cjs',
130 | ...settings
131 | },
132 | plugins,
133 | ...configBase
134 | }),
135 |
136 | // libultrahdr UMD
137 | defineConfig({
138 | input: './src/libultrahdr.ts',
139 | output: {
140 | format: 'umd',
141 | name: 'libultrahdr',
142 | file: 'dist/libultrahdr.umd.cjs',
143 | ...settings
144 | },
145 | plugins,
146 | ...configBase
147 | }),
148 |
149 | // worker interface umd
150 | defineConfig({
151 | input: './src/worker-interface.ts',
152 | output: {
153 | format: 'umd',
154 | name: 'worker-interface',
155 | file: 'dist/worker-interface.umd.cjs',
156 | ...settings
157 | },
158 | plugins,
159 | ...configBase
160 | })
161 | ])
162 | }
163 |
164 | export default configs
165 |
--------------------------------------------------------------------------------
/src/core/get-data-texture.ts:
--------------------------------------------------------------------------------
1 | import { DataTexture, LinearFilter, LinearSRGBColorSpace, RepeatWrapping, RGBAFormat, UVMapping } from 'three'
2 | import { EXR } from 'three/examples/jsm/loaders/EXRLoader'
3 | import { RGBE } from 'three/examples/jsm/loaders/RGBELoader'
4 | /**
5 | * Utility function to obtain a `DataTexture` from various input formats
6 | *
7 | * @category Utility
8 | * @group Utility
9 | *
10 | * @param image
11 | * @returns
12 | */
13 | export const getDataTexture = (image: EXR | RGBE | DataTexture) => {
14 | let dataTexture: DataTexture
15 |
16 | if (image instanceof DataTexture) {
17 | if (!(image.image.data instanceof Uint16Array) && !(image.image.data instanceof Float32Array)) {
18 | throw new Error('Provided image is not HDR')
19 | }
20 | dataTexture = image
21 | } else {
22 | dataTexture = new DataTexture(
23 | image.data,
24 | image.width,
25 | image.height,
26 | 'format' in image ? image.format : RGBAFormat,
27 | image.type,
28 | UVMapping,
29 | RepeatWrapping,
30 | RepeatWrapping,
31 | LinearFilter,
32 | LinearFilter,
33 | 1,
34 | 'colorSpace' in image && image.colorSpace === 'srgb' ? image.colorSpace : LinearSRGBColorSpace
35 | )
36 |
37 | // TODO: This tries to detect a raw RGBE and applies flipY
38 | // see if there's a better way to detect it?
39 | if ('header' in image && 'gamma' in image) {
40 | dataTexture.flipY = true
41 | }
42 | dataTexture.needsUpdate = true
43 | }
44 |
45 | return dataTexture
46 | }
47 |
--------------------------------------------------------------------------------
/src/core/index.ts:
--------------------------------------------------------------------------------
1 | export * from './get-data-texture'
2 | export * from './QuadRenderer'
3 | export * from './types'
4 |
--------------------------------------------------------------------------------
/src/core/types.ts:
--------------------------------------------------------------------------------
1 | import { Mapping, RenderTargetOptions } from 'three'
2 |
3 | /**
4 | * This is the Metadata stored in an encoded Gainmap which is used
5 | * to decode it and return an HDR image
6 | *
7 | * @category Specs
8 | * @group Specs
9 | */
10 | export type GainMapMetadata = {
11 | /**
12 | * This is the gamma to apply to the stored map values.
13 | * @defaultValue [1, 1, 1]
14 | * @remarks
15 | * * Typically you can use a gamma of 1.0.
16 | * * You can use a different value if your gain map has a very uneven distribution of log_recovery(x, y) values.
17 | *
18 | * For example, this might apply if a gain map has a lot of detail just above SDR range (represented as small log_recovery(x, y) values),
19 | * and a very large map_max_log2 for the top end of the HDR rendition's desired brightness (represented by large log_recovery(x, y) values).
20 | * In this case, you can use a map_gamma higher than 1.0 so that recovery(x, y) can precisely encode the detail in both the low end and high end of log_recovery(x, y).
21 | */
22 | gamma: [number, number, number]
23 | /**
24 | * This is log2 of the minimum display boost value for which the map is applied at all.
25 | *
26 | * @remarks
27 | * * This value also affects how much to apply the gain map based on the display boost.
28 | * * Must be 0.0 or greater.
29 | *
30 | * Logarithmic space
31 | */
32 | hdrCapacityMin: number
33 | /**
34 | * Stores the value of hdr_capacity_max. This is log2 of the maximum display boost value for which the map is applied completely.
35 | *
36 | * @remarks
37 | * * This value also affects how much to apply the gain map based on the display boost.
38 | * * Must be greater than hdrCapacityMin.
39 | * * Required.
40 | *
41 | * Logarithmic space
42 | */
43 | hdrCapacityMax: number
44 | /**
45 | * This is the offset to apply to the SDR pixel values during gain map generation and application
46 | * @defaultValue [1/64, 1/64, 1/64]
47 | */
48 | offsetSdr: [number, number, number]
49 | /**
50 | * This is the offset to apply to the HDR pixel values during gain map generation and application.
51 | * @defaultValue [1/64, 1/64, 1/64]
52 | */
53 | offsetHdr: [number, number, number]
54 | /**
55 | * This is log2 of min content boost, which is the minimum allowed ratio of
56 | * the linear luminance for the target HDR rendition relative to
57 | * (divided by) that of the SDR image, at a given pixel.
58 | */
59 | gainMapMin: [number, number, number]
60 | /**
61 | * This is log2 of max content boost, which is the maximum allowed ratio of
62 | * the linear luminance for the Target HDR rendition relative to
63 | * (divided by) that of the SDR image, at a given pixel.
64 | */
65 | gainMapMax: [number, number, number]
66 | }
67 |
68 | /**
69 | *
70 | */
71 | export type QuadRendererTextureOptions = Omit & {
72 | /**
73 | * @defaultValue 300
74 | */
75 | mapping?: Mapping,
76 | /**
77 | * @defaultValue 1
78 | */
79 | anisotropy?: number
80 | }
81 |
--------------------------------------------------------------------------------
/src/decode.ts:
--------------------------------------------------------------------------------
1 | export * from './decode/index'
2 |
--------------------------------------------------------------------------------
/src/decode/decode.ts:
--------------------------------------------------------------------------------
1 | import {
2 | HalfFloatType,
3 | LinearSRGBColorSpace,
4 | SRGBColorSpace
5 | } from 'three'
6 |
7 | import { QuadRenderer } from '../core/QuadRenderer'
8 | import { GainMapDecoderMaterial } from './materials/GainMapDecoderMaterial'
9 | import { DecodeParameters } from './types'
10 |
11 | /**
12 | * Decodes a gain map using a WebGLRenderTarget
13 | *
14 | * @category Decoding Functions
15 | * @group Decoding Functions
16 | * @example
17 | * import { decode } from '@monogrid/gainmap-js'
18 | * import {
19 | * Mesh,
20 | * MeshBasicMaterial,
21 | * PerspectiveCamera,
22 | * PlaneGeometry,
23 | * Scene,
24 | * TextureLoader,
25 | * WebGLRenderer
26 | * } from 'three'
27 | *
28 | * const renderer = new WebGLRenderer()
29 | *
30 | * const textureLoader = new TextureLoader()
31 | *
32 | * // load SDR Representation
33 | * const sdr = await textureLoader.loadAsync('sdr.jpg')
34 | * // load Gain map recovery image
35 | * const gainMap = await textureLoader.loadAsync('gainmap.jpg')
36 | * // load metadata
37 | * const metadata = await (await fetch('metadata.json')).json()
38 | *
39 | * const result = await decode({
40 | * sdr,
41 | * gainMap,
42 | * // this allows to use `result.renderTarget.texture` directly
43 | * renderer,
44 | * // this will restore the full HDR range
45 | * maxDisplayBoost: Math.pow(2, metadata.hdrCapacityMax),
46 | * ...metadata
47 | * })
48 | *
49 | * const scene = new Scene()
50 | * // `result` can be used to populate a Texture
51 | * const mesh = new Mesh(
52 | * new PlaneGeometry(),
53 | * new MeshBasicMaterial({ map: result.renderTarget.texture })
54 | * )
55 | * scene.add(mesh)
56 | * renderer.render(scene, new PerspectiveCamera())
57 | *
58 | * // result must be manually disposed
59 | * // when you are done using it
60 | * result.dispose()
61 | *
62 | * @param params
63 | * @returns
64 | * @throws {Error} if the WebGLRenderer fails to render the gain map
65 | */
66 | export const decode = (params: DecodeParameters): InstanceType>> => {
67 | const { sdr, gainMap, renderer } = params
68 |
69 | if (sdr.colorSpace !== SRGBColorSpace) {
70 | console.warn('SDR Colorspace needs to be *SRGBColorSpace*, setting it automatically')
71 | sdr.colorSpace = SRGBColorSpace
72 | }
73 | sdr.needsUpdate = true
74 |
75 | if (gainMap.colorSpace !== LinearSRGBColorSpace) {
76 | console.warn('Gainmap Colorspace needs to be *LinearSRGBColorSpace*, setting it automatically')
77 | gainMap.colorSpace = LinearSRGBColorSpace
78 | }
79 | gainMap.needsUpdate = true
80 |
81 | const material = new GainMapDecoderMaterial({
82 | ...params,
83 | sdr,
84 | gainMap
85 | })
86 | const quadRenderer = new QuadRenderer({
87 | // TODO: three types are generic, eslint complains here, see how we can solve
88 | // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-member-access
89 | width: sdr.image.width,
90 | // TODO: three types are generic, eslint complains here, see how we can solve
91 | // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-member-access
92 | height: sdr.image.height,
93 | type: HalfFloatType,
94 | colorSpace: LinearSRGBColorSpace,
95 | material,
96 | renderer,
97 | renderTargetOptions: params.renderTargetOptions
98 | })
99 | try {
100 | quadRenderer.render()
101 | } catch (e) {
102 | quadRenderer.disposeOnDemandRenderer()
103 | throw e
104 | }
105 | return quadRenderer
106 | }
107 |
--------------------------------------------------------------------------------
/src/decode/errors/GainMapNotFoundError.ts:
--------------------------------------------------------------------------------
1 | export class GainMapNotFoundError extends Error {}
2 |
--------------------------------------------------------------------------------
/src/decode/errors/XMPMetadataNotFoundError.ts:
--------------------------------------------------------------------------------
1 | export class XMPMetadataNotFoundError extends Error {}
2 |
--------------------------------------------------------------------------------
/src/decode/extract.ts:
--------------------------------------------------------------------------------
1 | import { GainMapNotFoundError } from './errors/GainMapNotFoundError'
2 | import { XMPMetadataNotFoundError } from './errors/XMPMetadataNotFoundError'
3 | import { extractXMP } from './utils/extractXMP'
4 | import { MPFExtractor } from './utils/MPFExtractor'
5 | /**
6 | * Extracts XMP Metadata and the gain map recovery image
7 | * from a single JPEG file.
8 | *
9 | * @category Decoding Functions
10 | * @group Decoding Functions
11 | * @param jpegFile an `Uint8Array` containing and encoded JPEG file
12 | * @returns an sdr `Uint8Array` compressed in JPEG, a gainMap `Uint8Array` compressed in JPEG and the XMP parsed XMP metadata
13 | * @throws Error if XMP Metadata is not found
14 | * @throws Error if Gain map image is not found
15 | * @example
16 | * import { FileLoader } from 'three'
17 | * import { extractGainmapFromJPEG } from '@monogrid/gainmap-js'
18 | *
19 | * const jpegFile = await new FileLoader()
20 | * .setResponseType('arraybuffer')
21 | * .loadAsync('image.jpg')
22 | *
23 | * const { sdr, gainMap, metadata } = extractGainmapFromJPEG(jpegFile)
24 | */
25 | export const extractGainmapFromJPEG = async (jpegFile: Uint8Array) => {
26 | const metadata = extractXMP(jpegFile)
27 | if (!metadata) throw new XMPMetadataNotFoundError('Gain map XMP metadata not found')
28 |
29 | const mpfExtractor = new MPFExtractor({ extractFII: true, extractNonFII: true })
30 | const images = await mpfExtractor.extract(jpegFile)
31 | if (images.length !== 2) throw new GainMapNotFoundError('Gain map recovery image not found')
32 |
33 | return {
34 | sdr: new Uint8Array(await images[0].arrayBuffer()),
35 | gainMap: new Uint8Array(await images[1].arrayBuffer()),
36 | metadata
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/src/decode/index.ts:
--------------------------------------------------------------------------------
1 | export * from '../core/QuadRenderer'
2 | export * from '../core/types'
3 | export * from './decode'
4 | export * from './extract'
5 | export * from './loaders/GainMapLoader'
6 | export * from './loaders/HDRJPGLoader'
7 | // Legacy name, TODO: can be removed with next breaking change release
8 | export { HDRJPGLoader as JPEGRLoader } from './loaders/HDRJPGLoader'
9 | export * from './materials/GainMapDecoderMaterial'
10 | export * from './types'
11 | export * from './utils/extractXMP'
12 | export * from './utils/MPFExtractor'
13 |
--------------------------------------------------------------------------------
/src/decode/loaders/HDRJPGLoader.ts:
--------------------------------------------------------------------------------
1 | import {
2 | FileLoader,
3 | HalfFloatType
4 | } from 'three'
5 |
6 | import { QuadRenderer } from '../../core/QuadRenderer'
7 | import { GainMapNotFoundError } from '../errors/GainMapNotFoundError'
8 | import { XMPMetadataNotFoundError } from '../errors/XMPMetadataNotFoundError'
9 | import { extractGainmapFromJPEG } from '../extract'
10 | import { GainMapMetadata } from '../index'
11 | import { GainMapDecoderMaterial } from '../materials/GainMapDecoderMaterial'
12 | import { LoaderBase } from './LoaderBase'
13 |
14 | /**
15 | * A Three.js Loader for a JPEG with embedded gainmap metadata.
16 | *
17 | * @category Loaders
18 | * @group Loaders
19 | *
20 | * @example
21 | * import { HDRJPGLoader } from '@monogrid/gainmap-js'
22 | * import {
23 | * EquirectangularReflectionMapping,
24 | * LinearFilter,
25 | * Mesh,
26 | * MeshBasicMaterial,
27 | * PerspectiveCamera,
28 | * PlaneGeometry,
29 | * Scene,
30 | * WebGLRenderer
31 | * } from 'three'
32 | *
33 | * const renderer = new WebGLRenderer()
34 | *
35 | * const loader = new HDRJPGLoader(renderer)
36 | *
37 | * const result = await loader.loadAsync('gainmap.jpeg')
38 | * // `result` can be used to populate a Texture
39 | *
40 | * const scene = new Scene()
41 | * const mesh = new Mesh(
42 | * new PlaneGeometry(),
43 | * new MeshBasicMaterial({ map: result.renderTarget.texture })
44 | * )
45 | * scene.add(mesh)
46 | * renderer.render(scene, new PerspectiveCamera())
47 | *
48 | * // Starting from three.js r159
49 | * // `result.renderTarget.texture` can
50 | * // also be used as Equirectangular scene background
51 | * //
52 | * // it was previously needed to convert it
53 | * // to a DataTexture with `result.toDataTexture()`
54 | * scene.background = result.renderTarget.texture
55 | * scene.background.mapping = EquirectangularReflectionMapping
56 | *
57 | * // result must be manually disposed
58 | * // when you are done using it
59 | * result.dispose()
60 | *
61 | */
62 | export class HDRJPGLoader extends LoaderBase {
63 | /**
64 | * Loads a JPEG containing gain map metadata
65 | * Renders a normal SDR image if gainmap data is not found
66 | *
67 | * @param url An array in the form of [sdr.jpg, gainmap.jpg, metadata.json]
68 | * @param onLoad Load complete callback, will receive the result
69 | * @param onProgress Progress callback, will receive a `ProgressEvent`
70 | * @param onError Error callback
71 | * @returns
72 | */
73 | public override load (url: string, onLoad?: (data: QuadRenderer) => void, onProgress?: (event: ProgressEvent) => void, onError?: (err: unknown) => void): QuadRenderer {
74 | const quadRenderer = this.prepareQuadRenderer()
75 |
76 | const loader = new FileLoader(this._internalLoadingManager)
77 | loader.setResponseType('arraybuffer')
78 | loader.setRequestHeader(this.requestHeader)
79 | loader.setPath(this.path)
80 | loader.setWithCredentials(this.withCredentials)
81 | this.manager.itemStart(url)
82 | loader.load(url, async (jpeg) => {
83 | /* istanbul ignore if
84 | this condition exists only because of three.js types + strict mode
85 | */
86 | if (typeof jpeg === 'string') throw new Error('Invalid buffer, received [string], was expecting [ArrayBuffer]')
87 | const jpegBuffer = new Uint8Array(jpeg)
88 | let sdrJPEG: Uint8Array
89 | let gainMapJPEG: Uint8Array | undefined
90 | let metadata: GainMapMetadata
91 | try {
92 | const extractionResult = await extractGainmapFromJPEG(jpegBuffer)
93 | // gain map is successfully reconstructed
94 | sdrJPEG = extractionResult.sdr
95 | gainMapJPEG = extractionResult.gainMap
96 | metadata = extractionResult.metadata
97 | } catch (e: unknown) {
98 | // render the SDR version if this is not a gainmap
99 | if (e instanceof XMPMetadataNotFoundError || e instanceof GainMapNotFoundError) {
100 | console.warn(`Failure to reconstruct an HDR image from ${url}: Gain map metadata not found in the file, HDRJPGLoader will render the SDR jpeg`)
101 | metadata = {
102 | gainMapMin: [0, 0, 0],
103 | gainMapMax: [1, 1, 1],
104 | gamma: [1, 1, 1],
105 | hdrCapacityMin: 0,
106 | hdrCapacityMax: 1,
107 | offsetHdr: [0, 0, 0],
108 | offsetSdr: [0, 0, 0]
109 | }
110 | sdrJPEG = jpegBuffer
111 | } else {
112 | throw e
113 | }
114 | }
115 |
116 | // solves #16
117 | try {
118 | await this.render(quadRenderer, metadata, sdrJPEG, gainMapJPEG)
119 | } catch (error) {
120 | this.manager.itemError(url)
121 | if (typeof onError === 'function') onError(error)
122 | quadRenderer.disposeOnDemandRenderer()
123 | return
124 | }
125 |
126 | if (typeof onLoad === 'function') onLoad(quadRenderer)
127 | this.manager.itemEnd(url)
128 | quadRenderer.disposeOnDemandRenderer()
129 | }, onProgress
130 | , (error: unknown) => {
131 | this.manager.itemError(url)
132 | if (typeof onError === 'function') onError(error)
133 | })
134 |
135 | return quadRenderer
136 | }
137 | }
138 |
--------------------------------------------------------------------------------
/src/decode/loaders/LoaderBase.ts:
--------------------------------------------------------------------------------
1 | import {
2 | ClampToEdgeWrapping,
3 | HalfFloatType,
4 | LinearFilter,
5 | LinearMipMapLinearFilter,
6 | LinearSRGBColorSpace,
7 | Loader,
8 | LoadingManager,
9 | RGBAFormat,
10 | SRGBColorSpace,
11 | Texture,
12 | UnsignedByteType,
13 | UVMapping,
14 | WebGLRenderer
15 | } from 'three'
16 |
17 | import { QuadRenderer } from '../../core/QuadRenderer'
18 | import { type GainMapMetadata, QuadRendererTextureOptions } from '../../core/types'
19 | import { GainMapDecoderMaterial } from '../materials/GainMapDecoderMaterial'
20 | import { getHTMLImageFromBlob } from '../utils/get-html-image-from-blob'
21 |
22 | export class LoaderBase extends Loader, TUrl> {
23 | private _renderer?: WebGLRenderer
24 | private _renderTargetOptions?: QuadRendererTextureOptions
25 | /**
26 | * @private
27 | */
28 | protected _internalLoadingManager: LoadingManager
29 | /**
30 | *
31 | * @param renderer
32 | * @param manager
33 | */
34 | constructor (renderer?: WebGLRenderer, manager?: LoadingManager) {
35 | super(manager)
36 | if (renderer) this._renderer = renderer
37 | this._internalLoadingManager = new LoadingManager()
38 | }
39 |
40 | /**
41 | * Specify the renderer to use when rendering the gain map
42 | *
43 | * @param renderer
44 | * @returns
45 | */
46 | public setRenderer (renderer: WebGLRenderer) {
47 | this._renderer = renderer
48 | return this
49 | }
50 |
51 | /**
52 | * Specify the renderTarget options to use when rendering the gain map
53 | *
54 | * @param options
55 | * @returns
56 | */
57 | public setRenderTargetOptions (options: QuadRendererTextureOptions) {
58 | this._renderTargetOptions = options
59 | return this
60 | }
61 |
62 | /**
63 | * @private
64 | * @returns
65 | */
66 | protected prepareQuadRenderer () {
67 | if (!this._renderer) console.warn('WARNING: An existing WebGL Renderer was not passed to this Loader constructor or in setRenderer, the result of this Loader will need to be converted to a Data Texture with toDataTexture() before you can use it in your renderer.')
68 |
69 | // temporary values
70 | const material = new GainMapDecoderMaterial({
71 | gainMapMax: [1, 1, 1],
72 | gainMapMin: [0, 0, 0],
73 | gamma: [1, 1, 1],
74 | offsetHdr: [1, 1, 1],
75 | offsetSdr: [1, 1, 1],
76 | hdrCapacityMax: 1,
77 | hdrCapacityMin: 0,
78 | maxDisplayBoost: 1,
79 | gainMap: new Texture(),
80 | sdr: new Texture()
81 | })
82 |
83 | return new QuadRenderer({
84 | width: 16,
85 | height: 16,
86 | type: HalfFloatType,
87 | colorSpace: LinearSRGBColorSpace,
88 | material,
89 | renderer: this._renderer,
90 | renderTargetOptions: this._renderTargetOptions
91 | })
92 | }
93 |
94 | /**
95 | * @private
96 | * @param quadRenderer
97 | * @param metadata
98 | * @param sdrBuffer
99 | * @param gainMapBuffer
100 | */
101 | protected async render (quadRenderer: QuadRenderer, metadata: GainMapMetadata, sdrBuffer: ArrayBuffer, gainMapBuffer?: ArrayBuffer) {
102 | // this is optional, will render a black gain-map if not present
103 | const gainMapBlob = gainMapBuffer ? new Blob([gainMapBuffer], { type: 'image/jpeg' }) : undefined
104 |
105 | const sdrBlob = new Blob([sdrBuffer], { type: 'image/jpeg' })
106 |
107 | let sdrImage: ImageBitmap | HTMLImageElement
108 | let gainMapImage: ImageBitmap | HTMLImageElement | undefined
109 |
110 | let needsFlip = false
111 |
112 | if (typeof createImageBitmap === 'undefined') {
113 | const res = await Promise.all([
114 | gainMapBlob ? getHTMLImageFromBlob(gainMapBlob) : Promise.resolve(undefined),
115 | getHTMLImageFromBlob(sdrBlob)
116 | ])
117 |
118 | gainMapImage = res[0]
119 | sdrImage = res[1]
120 |
121 | needsFlip = true
122 | } else {
123 | const res = await Promise.all([
124 | gainMapBlob ? createImageBitmap(gainMapBlob, { imageOrientation: 'flipY' }) : Promise.resolve(undefined),
125 | createImageBitmap(sdrBlob, { imageOrientation: 'flipY' })
126 | ])
127 |
128 | gainMapImage = res[0]
129 | sdrImage = res[1]
130 | }
131 |
132 | const gainMap = new Texture(gainMapImage || new ImageData(2, 2),
133 | UVMapping,
134 | ClampToEdgeWrapping,
135 | ClampToEdgeWrapping,
136 | LinearFilter,
137 | LinearMipMapLinearFilter,
138 | RGBAFormat,
139 | UnsignedByteType,
140 | 1,
141 | LinearSRGBColorSpace
142 | )
143 |
144 | gainMap.flipY = needsFlip
145 | gainMap.needsUpdate = true
146 |
147 | const sdr = new Texture(sdrImage,
148 | UVMapping,
149 | ClampToEdgeWrapping,
150 | ClampToEdgeWrapping,
151 | LinearFilter,
152 | LinearMipMapLinearFilter,
153 | RGBAFormat,
154 | UnsignedByteType,
155 | 1,
156 | SRGBColorSpace
157 | )
158 |
159 | sdr.flipY = needsFlip
160 | sdr.needsUpdate = true
161 |
162 | quadRenderer.width = sdrImage.width
163 | quadRenderer.height = sdrImage.height
164 | quadRenderer.material.gainMap = gainMap
165 | quadRenderer.material.sdr = sdr
166 | quadRenderer.material.gainMapMin = metadata.gainMapMin
167 | quadRenderer.material.gainMapMax = metadata.gainMapMax
168 | quadRenderer.material.offsetHdr = metadata.offsetHdr
169 | quadRenderer.material.offsetSdr = metadata.offsetSdr
170 | quadRenderer.material.gamma = metadata.gamma
171 | quadRenderer.material.hdrCapacityMin = metadata.hdrCapacityMin
172 | quadRenderer.material.hdrCapacityMax = metadata.hdrCapacityMax
173 | quadRenderer.material.maxDisplayBoost = Math.pow(2, metadata.hdrCapacityMax)
174 | quadRenderer.material.needsUpdate = true
175 |
176 | quadRenderer.render()
177 | }
178 | }
179 |
--------------------------------------------------------------------------------
/src/decode/materials/GainMapDecoderMaterial.ts:
--------------------------------------------------------------------------------
1 | import { NoBlending, ShaderMaterial, Texture, Vector3 } from 'three'
2 |
3 | import { GainMapMetadata } from '../../core/types'
4 | import { GainmapDecodingParameters } from '../types'
5 |
6 | const vertexShader = /* glsl */`
7 | varying vec2 vUv;
8 |
9 | void main() {
10 | vUv = uv;
11 | gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
12 | }
13 | `
14 |
15 | const fragmentShader = /* glsl */`
16 | // min half float value
17 | #define HALF_FLOAT_MIN vec3( -65504, -65504, -65504 )
18 | // max half float value
19 | #define HALF_FLOAT_MAX vec3( 65504, 65504, 65504 )
20 |
21 | uniform sampler2D sdr;
22 | uniform sampler2D gainMap;
23 | uniform vec3 gamma;
24 | uniform vec3 offsetHdr;
25 | uniform vec3 offsetSdr;
26 | uniform vec3 gainMapMin;
27 | uniform vec3 gainMapMax;
28 | uniform float weightFactor;
29 |
30 | varying vec2 vUv;
31 |
32 | void main() {
33 | vec3 rgb = texture2D( sdr, vUv ).rgb;
34 | vec3 recovery = texture2D( gainMap, vUv ).rgb;
35 | vec3 logRecovery = pow( recovery, gamma );
36 | vec3 logBoost = gainMapMin * ( 1.0 - logRecovery ) + gainMapMax * logRecovery;
37 | vec3 hdrColor = (rgb + offsetSdr) * exp2( logBoost * weightFactor ) - offsetHdr;
38 | vec3 clampedHdrColor = max( HALF_FLOAT_MIN, min( HALF_FLOAT_MAX, hdrColor ));
39 | gl_FragColor = vec4( clampedHdrColor , 1.0 );
40 | }
41 | `
42 | /**
43 | * A Material which is able to decode the Gainmap into a full HDR Representation
44 | *
45 | * @category Materials
46 | * @group Materials
47 | */
48 | export class GainMapDecoderMaterial extends ShaderMaterial {
49 | private _maxDisplayBoost: GainmapDecodingParameters['maxDisplayBoost']
50 | private _hdrCapacityMin: GainMapMetadata['hdrCapacityMin']
51 | private _hdrCapacityMax: GainMapMetadata['hdrCapacityMax']
52 | /**
53 | *
54 | * @param params
55 | */
56 | constructor ({ gamma, offsetHdr, offsetSdr, gainMapMin, gainMapMax, maxDisplayBoost, hdrCapacityMin, hdrCapacityMax, sdr, gainMap }: GainMapMetadata & GainmapDecodingParameters & { sdr: Texture, gainMap: Texture }) {
57 | super({
58 | name: 'GainMapDecoderMaterial',
59 | vertexShader,
60 | fragmentShader,
61 | uniforms: {
62 | sdr: { value: sdr },
63 | gainMap: { value: gainMap },
64 | gamma: { value: new Vector3(1.0 / gamma[0], 1.0 / gamma[1], 1.0 / gamma[2]) },
65 | offsetHdr: { value: new Vector3().fromArray(offsetHdr) },
66 | offsetSdr: { value: new Vector3().fromArray(offsetSdr) },
67 | gainMapMin: { value: new Vector3().fromArray(gainMapMin) },
68 | gainMapMax: { value: new Vector3().fromArray(gainMapMax) },
69 | weightFactor: {
70 | value: (Math.log2(maxDisplayBoost) - hdrCapacityMin) / (hdrCapacityMax - hdrCapacityMin)
71 | }
72 | },
73 | blending: NoBlending,
74 | depthTest: false,
75 | depthWrite: false
76 | })
77 |
78 | this._maxDisplayBoost = maxDisplayBoost
79 | this._hdrCapacityMin = hdrCapacityMin
80 | this._hdrCapacityMax = hdrCapacityMax
81 |
82 | this.needsUpdate = true
83 | this.uniformsNeedUpdate = true
84 | }
85 |
86 | get sdr () { return this.uniforms.sdr.value as Texture }
87 | set sdr (value: Texture) { this.uniforms.sdr.value = value }
88 |
89 | get gainMap () { return this.uniforms.gainMap.value as Texture }
90 | set gainMap (value: Texture) { this.uniforms.gainMap.value = value }
91 | /**
92 | * @see {@link GainMapMetadata.offsetHdr}
93 | */
94 | get offsetHdr () { return (this.uniforms.offsetHdr.value as Vector3).toArray() }
95 | set offsetHdr (value: [number, number, number]) { (this.uniforms.offsetHdr.value as Vector3).fromArray(value) }
96 | /**
97 | * @see {@link GainMapMetadata.offsetSdr}
98 | */
99 | get offsetSdr () { return (this.uniforms.offsetSdr.value as Vector3).toArray() }
100 | set offsetSdr (value: [number, number, number]) { (this.uniforms.offsetSdr.value as Vector3).fromArray(value) }
101 | /**
102 | * @see {@link GainMapMetadata.gainMapMin}
103 | */
104 | get gainMapMin () { return (this.uniforms.gainMapMin.value as Vector3).toArray() }
105 | set gainMapMin (value: [number, number, number]) { (this.uniforms.gainMapMin.value as Vector3).fromArray(value) }
106 | /**
107 | * @see {@link GainMapMetadata.gainMapMax}
108 | */
109 | get gainMapMax () { return (this.uniforms.gainMapMax.value as Vector3).toArray() }
110 | set gainMapMax (value: [number, number, number]) { (this.uniforms.gainMapMax.value as Vector3).fromArray(value) }
111 |
112 | /**
113 | * @see {@link GainMapMetadata.gamma}
114 | */
115 | get gamma () {
116 | const g = this.uniforms.gamma.value as Vector3
117 | return [1 / g.x, 1 / g.y, 1 / g.z] as [number, number, number]
118 | }
119 |
120 | set gamma (value: [number, number, number]) {
121 | const g = this.uniforms.gamma.value as Vector3
122 | g.x = 1.0 / value[0]
123 | g.y = 1.0 / value[1]
124 | g.z = 1.0 / value[2]
125 | }
126 |
127 | /**
128 | * @see {@link GainMapMetadata.hdrCapacityMin}
129 | * @remarks Logarithmic space
130 | */
131 | get hdrCapacityMin () { return this._hdrCapacityMin }
132 | set hdrCapacityMin (value: number) {
133 | this._hdrCapacityMin = value
134 | this.calculateWeight()
135 | }
136 |
137 | /**
138 | * @see {@link GainMapMetadata.hdrCapacityMin}
139 | * @remarks Logarithmic space
140 | */
141 | get hdrCapacityMax () { return this._hdrCapacityMax }
142 | set hdrCapacityMax (value: number) {
143 | this._hdrCapacityMax = value
144 | this.calculateWeight()
145 | }
146 |
147 | /**
148 | * @see {@link GainmapDecodingParameters.maxDisplayBoost}
149 | * @remarks Non Logarithmic space
150 | */
151 | get maxDisplayBoost () { return this._maxDisplayBoost }
152 | set maxDisplayBoost (value: number) {
153 | this._maxDisplayBoost = Math.max(1, Math.min(65504, value))
154 | this.calculateWeight()
155 | }
156 |
157 | private calculateWeight () {
158 | const val = (Math.log2(this._maxDisplayBoost) - this._hdrCapacityMin) / (this._hdrCapacityMax - this._hdrCapacityMin)
159 | this.uniforms.weightFactor.value = Math.max(0, Math.min(1, val))
160 | }
161 | }
162 |
--------------------------------------------------------------------------------
/src/decode/types.ts:
--------------------------------------------------------------------------------
1 | import { type Texture, type WebGLRenderer } from 'three'
2 |
3 | import { type GainMapMetadata, type QuadRendererTextureOptions } from '../core/types'
4 |
5 | /**
6 | * Necessary parameters for decoding a Gainmap
7 | *
8 | * @category Specs
9 | * @group Specs
10 | */
11 | export type GainmapDecodingParameters = {
12 | /**
13 | * The maximum available boost supported by a display, at a given point in time.
14 | *
15 | * @remarks
16 | * This value can change over time based on device settings and other factors,
17 | * such as ambient light conditions, or how many bright pixels are on the screen.
18 | *
19 | * Non Logarithmic space
20 | */
21 | maxDisplayBoost: number
22 | }
23 | /**
24 | * @category Decoding Functions
25 | * @group Decoding Functions
26 | */
27 | export type DecodeParameters = {
28 | /**
29 | * An Texture containing the SDR Rendition
30 | */
31 | sdr: Texture
32 | /**
33 | * An Texture containing the GainMap recovery image
34 | */
35 | gainMap: Texture
36 | /**
37 | * WebGLRenderer used to decode the GainMap
38 | */
39 | renderer?: WebGLRenderer,
40 | /**
41 | * Options to use when creating the output renderTarget
42 | */
43 | renderTargetOptions?: QuadRendererTextureOptions
44 |
45 | } & GainmapDecodingParameters & GainMapMetadata
46 |
--------------------------------------------------------------------------------
/src/decode/utils/extractXMP.ts:
--------------------------------------------------------------------------------
1 | import { GainMapMetadata } from '../../core/types'
2 |
3 | const getXMLValue = (xml: string, tag: string, defaultValue?: string): string | [string, string, string] => {
4 | // Check for attribute format first: tag="value"
5 | const attributeMatch = new RegExp(`${tag}="([^"]*)"`, 'i').exec(xml)
6 | if (attributeMatch) return attributeMatch[1]
7 |
8 | // Check for tag format: value or value...
9 | const tagMatch = new RegExp(`<${tag}[^>]*>([\\s\\S]*?)${tag}>`, 'i').exec(xml)
10 | if (tagMatch) {
11 | // Check if it contains rdf:li elements
12 | const liValues = tagMatch[1].match(/([^<]*)<\/rdf:li>/g)
13 | if (liValues && liValues.length === 3) {
14 | return liValues.map(v => v.replace(/<\/?rdf:li>/g, '')) as [string, string, string]
15 | }
16 | return tagMatch[1].trim()
17 | }
18 |
19 | if (defaultValue !== undefined) return defaultValue
20 | throw new Error(`Can't find ${tag} in gainmap metadata`)
21 | }
22 |
23 | export const extractXMP = (input: Uint8Array): GainMapMetadata | undefined => {
24 | let str: string
25 | // support node test environment
26 | if (typeof TextDecoder !== 'undefined') str = new TextDecoder().decode(input)
27 | else str = input.toString()
28 |
29 | let start = str.indexOf('', start)
33 | const xmpBlock = str.slice(start, end + 10)
34 |
35 | try {
36 | const gainMapMin = getXMLValue(xmpBlock, 'hdrgm:GainMapMin', '0')
37 | const gainMapMax = getXMLValue(xmpBlock, 'hdrgm:GainMapMax')
38 | const gamma = getXMLValue(xmpBlock, 'hdrgm:Gamma', '1')
39 | const offsetSDR = getXMLValue(xmpBlock, 'hdrgm:OffsetSDR', '0.015625')
40 | const offsetHDR = getXMLValue(xmpBlock, 'hdrgm:OffsetHDR', '0.015625')
41 |
42 | // These are always attributes, so we can use a simpler regex
43 | const hdrCapacityMinMatch = /hdrgm:HDRCapacityMin="([^"]*)"/.exec(xmpBlock)
44 | const hdrCapacityMin = hdrCapacityMinMatch ? hdrCapacityMinMatch[1] : '0'
45 |
46 | const hdrCapacityMaxMatch = /hdrgm:HDRCapacityMax="([^"]*)"/.exec(xmpBlock)
47 | if (!hdrCapacityMaxMatch) throw new Error('Incomplete gainmap metadata')
48 | const hdrCapacityMax = hdrCapacityMaxMatch[1]
49 |
50 | return {
51 | gainMapMin: Array.isArray(gainMapMin) ? gainMapMin.map(v => parseFloat(v)) as [number, number, number] : [parseFloat(gainMapMin), parseFloat(gainMapMin), parseFloat(gainMapMin)],
52 | gainMapMax: Array.isArray(gainMapMax) ? gainMapMax.map(v => parseFloat(v)) as [number, number, number] : [parseFloat(gainMapMax), parseFloat(gainMapMax), parseFloat(gainMapMax)],
53 | gamma: Array.isArray(gamma) ? gamma.map(v => parseFloat(v)) as [number, number, number] : [parseFloat(gamma), parseFloat(gamma), parseFloat(gamma)],
54 | offsetSdr: Array.isArray(offsetSDR) ? offsetSDR.map(v => parseFloat(v)) as [number, number, number] : [parseFloat(offsetSDR), parseFloat(offsetSDR), parseFloat(offsetSDR)],
55 | offsetHdr: Array.isArray(offsetHDR) ? offsetHDR.map(v => parseFloat(v)) as [number, number, number] : [parseFloat(offsetHDR), parseFloat(offsetHDR), parseFloat(offsetHDR)],
56 | hdrCapacityMin: parseFloat(hdrCapacityMin),
57 | hdrCapacityMax: parseFloat(hdrCapacityMax)
58 | }
59 | } catch (e) {
60 | // Continue searching for another xmpmeta block if this one fails
61 | }
62 | start = str.indexOf(' {
8 | return new Promise((resolve, reject) => {
9 | const img = document.createElement('img')
10 | img.onload = () => { resolve(img) }
11 | // eslint-disable-next-line @typescript-eslint/prefer-promise-reject-errors
12 | img.onerror = (e) => { reject(e) }
13 | img.src = URL.createObjectURL(blob)
14 | })
15 | }
16 |
--------------------------------------------------------------------------------
/src/encode.ts:
--------------------------------------------------------------------------------
1 | export * from './encode/index'
2 |
--------------------------------------------------------------------------------
/src/encode/compress.ts:
--------------------------------------------------------------------------------
1 | import { CompressedImage, CompressParameters } from './types'
2 |
3 | /**
4 | * Used internally
5 | *
6 | * @internal
7 | * @param canvas
8 | * @param mimeType
9 | * @param quality
10 | * @returns
11 | */
12 | const canvasToBlob = async (canvas: OffscreenCanvas | HTMLCanvasElement, mimeType: CompressParameters['mimeType'], quality: CompressParameters['quality']) => {
13 | if (typeof OffscreenCanvas !== 'undefined' && canvas instanceof OffscreenCanvas) {
14 | return canvas.convertToBlob({ type: mimeType, quality: quality || 0.9 })
15 | } else if (canvas instanceof HTMLCanvasElement) {
16 | return new Promise((resolve, reject) => {
17 | canvas.toBlob((res) => {
18 | if (res) resolve(res)
19 | else reject(new Error('Failed to convert canvas to blob'))
20 | }, mimeType, quality || 0.9)
21 | })
22 | }
23 | /* istanbul ignore next
24 | as long as this function is not exported this is only here
25 | to satisfy TS strict mode internally
26 | */
27 | throw new Error('Unsupported canvas element')
28 | }
29 |
30 | /**
31 | * Converts a RAW RGBA image buffer into the provided `mimeType` using the provided `quality`
32 | *
33 | * @category Compression
34 | * @group Compression
35 | * @param params
36 | * @throws {Error} if the browser does not support [createImageBitmap](https://caniuse.com/createimagebitmap)
37 | * @throws {Error} if the provided source image cannot be decoded
38 | * @throws {Error} if the function fails to create a canvas context
39 | */
40 | export const compress = async (params: CompressParameters): Promise => {
41 | if (typeof createImageBitmap === 'undefined') throw new Error('createImageBitmap() not supported.')
42 | const { source, mimeType, quality, flipY } = params
43 |
44 | let imageBitmapSource: ImageBitmapSource
45 | if ((source instanceof Uint8Array || source instanceof Uint8ClampedArray) && 'sourceMimeType' in params) {
46 | imageBitmapSource = new Blob([source], { type: params.sourceMimeType })
47 | } else if (source instanceof ImageData) {
48 | imageBitmapSource = source
49 | } else {
50 | throw new Error('Invalid source image')
51 | }
52 | const img = await createImageBitmap(imageBitmapSource)
53 | const width = img.width
54 | const height = img.height
55 |
56 | let canvas: OffscreenCanvas | HTMLCanvasElement
57 | if (typeof OffscreenCanvas !== 'undefined') {
58 | canvas = new OffscreenCanvas(width, height)
59 | } else {
60 | canvas = document.createElement('canvas')
61 | canvas.width = width
62 | canvas.height = height
63 | }
64 | const ctx = canvas.getContext('2d')
65 | if (!ctx) throw new Error('Failed to create canvas Context')
66 | // flip Y
67 | if (flipY === true) {
68 | ctx.translate(0, height)
69 | ctx.scale(1, -1)
70 | }
71 |
72 | ctx.drawImage(img, 0, 0, width, height)
73 |
74 | const blob = await canvasToBlob(canvas, mimeType, quality || 0.9)
75 |
76 | const data = new Uint8Array(await blob.arrayBuffer())
77 |
78 | return {
79 | data,
80 | mimeType,
81 | width,
82 | height
83 | }
84 | }
85 |
--------------------------------------------------------------------------------
/src/encode/encode-and-compress.ts:
--------------------------------------------------------------------------------
1 | import { compress } from './compress'
2 | import { encode } from './encode'
3 | import { CompressedImage, EncodingParametersWithCompression } from './types'
4 |
5 | /**
6 | * Encodes a Gainmap starting from an HDR file into compressed file formats (`image/jpeg`, `image/webp` or `image/png`).
7 | *
8 | * Uses {@link encode} internally, then pipes the results to {@link compress}.
9 | *
10 | * @remarks
11 | * if a `renderer` parameter is not provided
12 | * This function will automatically dispose its "disposable"
13 | * renderer, no need to dispose it manually later
14 | *
15 | * @category Encoding Functions
16 | * @group Encoding Functions
17 | * @example
18 | * import { encodeAndCompress, findTextureMinMax } from '@monogrid/gainmap-js'
19 | * import { encodeJPEGMetadata } from '@monogrid/gainmap-js/libultrahdr'
20 | * import { EXRLoader } from 'three/examples/jsm/loaders/EXRLoader.js'
21 | *
22 | * // load an HDR file
23 | * const loader = new EXRLoader()
24 | * const image = await loader.loadAsync('image.exr')
25 | *
26 | * // find RAW RGB Max value of a texture
27 | * const textureMax = await findTextureMinMax(image)
28 | *
29 | * // Encode the gainmap
30 | * const encodingResult = await encodeAndCompress({
31 | * image,
32 | * maxContentBoost: Math.max.apply(this, textureMax),
33 | * mimeType: 'image/jpeg'
34 | * })
35 | *
36 | * // embed the compressed images + metadata into a single
37 | * // JPEG file
38 | * const jpeg = await encodeJPEGMetadata({
39 | * ...encodingResult,
40 | * sdr: encodingResult.sdr,
41 | * gainMap: encodingResult.gainMap
42 | * })
43 | *
44 | * // `jpeg` will be an `Uint8Array` which can be saved somewhere
45 | *
46 | *
47 | * @param params Encoding Parameters
48 | * @throws {Error} if the browser does not support [createImageBitmap](https://caniuse.com/createimagebitmap)
49 | */
50 | export const encodeAndCompress = async (params: EncodingParametersWithCompression) => {
51 | const encodingResult = encode(params)
52 |
53 | const { mimeType, quality, flipY, withWorker } = params
54 |
55 | let compressResult: [CompressedImage, CompressedImage]
56 |
57 | let rawSDR: Uint8ClampedArray
58 | let rawGainMap: Uint8ClampedArray
59 |
60 | const sdrImageData = new ImageData(encodingResult.sdr.toArray(), encodingResult.sdr.width, encodingResult.sdr.height)
61 | const gainMapImageData = new ImageData(encodingResult.gainMap.toArray(), encodingResult.gainMap.width, encodingResult.gainMap.height)
62 |
63 | if (withWorker) {
64 | const workerResult = await Promise.all([
65 | withWorker.compress({
66 | source: sdrImageData,
67 | mimeType,
68 | quality,
69 | flipY
70 | }),
71 | withWorker.compress({
72 | source: gainMapImageData,
73 | mimeType,
74 | quality,
75 | flipY
76 | })
77 | ])
78 | compressResult = workerResult
79 | rawSDR = workerResult[0].source
80 | rawGainMap = workerResult[1].source
81 | } else {
82 | compressResult = await Promise.all([
83 | compress({
84 | source: sdrImageData,
85 | mimeType,
86 | quality,
87 | flipY
88 | }),
89 | compress({
90 | source: gainMapImageData,
91 | mimeType,
92 | quality,
93 | flipY
94 | })
95 | ])
96 | rawSDR = sdrImageData.data
97 | rawGainMap = gainMapImageData.data
98 | }
99 |
100 | encodingResult.sdr.dispose()
101 | encodingResult.gainMap.dispose()
102 |
103 | return {
104 | ...encodingResult,
105 | ...encodingResult.getMetadata(),
106 | sdr: compressResult[0],
107 | gainMap: compressResult[1],
108 | rawSDR,
109 | rawGainMap
110 | }
111 | }
112 |
--------------------------------------------------------------------------------
/src/encode/encode.ts:
--------------------------------------------------------------------------------
1 | import { getDataTexture } from '../core/get-data-texture'
2 | import { GainMapMetadata } from '../core/types'
3 | import { getGainMap } from './get-gainmap'
4 | import { getSDRRendition } from './get-sdr-rendition'
5 | import { EncodingParametersBase } from './types'
6 |
7 | /**
8 | * Encodes a Gainmap starting from an HDR file.
9 | *
10 | * @remarks
11 | * if you do not pass a `renderer` parameter
12 | * you must manually dispose the result
13 | * ```js
14 | * const encodingResult = await encode({ ... })
15 | * // do something with the buffers
16 | * const sdr = encodingResult.sdr.getArray()
17 | * const gainMap = encodingResult.gainMap.getArray()
18 | * // after that
19 | * encodingResult.sdr.dispose()
20 | * encodingResult.gainMap.dispose()
21 | * ```
22 | *
23 | * @category Encoding Functions
24 | * @group Encoding Functions
25 | *
26 | * @example
27 | * import { encode, findTextureMinMax } from '@monogrid/gainmap-js'
28 | * import { EXRLoader } from 'three/examples/jsm/loaders/EXRLoader.js'
29 | *
30 | * // load an HDR file
31 | * const loader = new EXRLoader()
32 | * const image = await loader.loadAsync('image.exr')
33 | *
34 | * // find RAW RGB Max value of a texture
35 | * const textureMax = await findTextureMinMax(image)
36 | *
37 | * // Encode the gainmap
38 | * const encodingResult = encode({
39 | * image,
40 | * // this will encode the full HDR range
41 | * maxContentBoost: Math.max.apply(this, textureMax)
42 | * })
43 | * // can be re-encoded after changing parameters
44 | * encodingResult.sdr.material.exposure = 0.9
45 | * encodingResult.sdr.render()
46 | * // or
47 | * encodingResult.gainMap.material.gamma = [1.1, 1.1, 1.1]
48 | * encodingResult.gainMap.render()
49 | *
50 | * // do something with encodingResult.gainMap.toArray()
51 | * // and encodingResult.sdr.toArray()
52 | *
53 | * // renderers must be manually disposed
54 | * encodingResult.sdr.dispose()
55 | * encodingResult.gainMap.dispose()
56 | *
57 | * @param params Encoding Parameters
58 | * @returns
59 | */
60 | export const encode = (params: EncodingParametersBase) => {
61 | const { image, renderer } = params
62 |
63 | const dataTexture = getDataTexture(image)
64 |
65 | const sdr = getSDRRendition(dataTexture, renderer, params.toneMapping, params.renderTargetOptions)
66 |
67 | const gainMapRenderer = getGainMap({
68 | ...params,
69 | image: dataTexture,
70 | sdr,
71 | renderer: sdr.renderer // reuse the same (maybe disposable?) renderer
72 | })
73 |
74 | return {
75 | sdr,
76 | gainMap: gainMapRenderer,
77 | hdr: dataTexture,
78 | getMetadata: (): GainMapMetadata => {
79 | return {
80 | gainMapMax: gainMapRenderer.material.gainMapMax,
81 | gainMapMin: gainMapRenderer.material.gainMapMin,
82 | gamma: gainMapRenderer.material.gamma,
83 | hdrCapacityMax: gainMapRenderer.material.hdrCapacityMax,
84 | hdrCapacityMin: gainMapRenderer.material.hdrCapacityMin,
85 | offsetHdr: gainMapRenderer.material.offsetHdr,
86 | offsetSdr: gainMapRenderer.material.offsetSdr
87 | }
88 | }
89 | }
90 | }
91 |
--------------------------------------------------------------------------------
/src/encode/find-texture-min-max.ts:
--------------------------------------------------------------------------------
1 | import {
2 | ClampToEdgeWrapping,
3 | ColorSpace,
4 | DataTexture,
5 | DataUtils,
6 | FloatType,
7 | NearestFilter,
8 | ShaderMaterial,
9 | Vector2,
10 | WebGLRenderer,
11 | WebGLRenderTarget
12 | } from 'three'
13 | import { EXR } from 'three/examples/jsm/loaders/EXRLoader'
14 | import { RGBE } from 'three/examples/jsm/loaders/RGBELoader'
15 |
16 | import { getDataTexture } from '../core/get-data-texture'
17 | import { QuadRenderer } from '../core/QuadRenderer'
18 | const vertexShader = /* glsl */`
19 | varying vec2 vUv;
20 | void main() {
21 | vUv = uv;
22 | gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
23 | }
24 | `
25 |
26 | const fragmentShader = /* glsl */`
27 | precision mediump float;
28 |
29 | #ifndef CELL_SIZE
30 | #define CELL_SIZE 2
31 | #endif
32 |
33 | #ifndef COMPARE_FUNCTION
34 | #define COMPARE_FUNCTION max
35 | #endif
36 |
37 | #ifndef INITIAL_VALUE
38 | #define INITIAL_VALUE 0
39 | #endif
40 |
41 | uniform sampler2D map;
42 | uniform vec2 u_srcResolution;
43 |
44 | varying vec2 vUv;
45 |
46 | void main() {
47 | // compute the first pixel the source cell
48 | vec2 srcPixel = floor(gl_FragCoord.xy) * float(CELL_SIZE);
49 |
50 | // one pixel in source
51 | vec2 onePixel = vec2(1) / u_srcResolution;
52 |
53 | // uv for first pixel in cell. +0.5 for center of pixel
54 | vec2 uv = (srcPixel + 0.5) * onePixel;
55 |
56 | vec4 resultColor = vec4(INITIAL_VALUE);
57 |
58 | for (int y = 0; y < CELL_SIZE; ++y) {
59 | for (int x = 0; x < CELL_SIZE; ++x) {
60 | resultColor = COMPARE_FUNCTION(resultColor, texture2D(map, uv + vec2(x, y) * onePixel));
61 | }
62 | }
63 |
64 | gl_FragColor = resultColor;
65 | }
66 | `
67 | /**
68 | *
69 | * @category Utility
70 | * @group Utility
71 | *
72 | * @param image
73 | * @param mode
74 | * @param renderer
75 | * @returns
76 | */
77 | export const findTextureMinMax = (image: EXR | RGBE | DataTexture, mode: 'min' | 'max' = 'max', renderer?: WebGLRenderer) => {
78 | const srcTex = getDataTexture(image)
79 | const cellSize = 2
80 |
81 | const mat = new ShaderMaterial({
82 | vertexShader,
83 | fragmentShader,
84 | uniforms: {
85 | u_srcResolution: { value: new Vector2(srcTex.image.width, srcTex.image.height) },
86 | map: { value: srcTex }
87 | },
88 | defines: {
89 | CELL_SIZE: cellSize,
90 | COMPARE_FUNCTION: mode,
91 | INITIAL_VALUE: mode === 'max' ? 0 : 65504 // max half float value
92 | }
93 | })
94 | srcTex.needsUpdate = true
95 | mat.needsUpdate = true
96 |
97 | let w = srcTex.image.width
98 | let h = srcTex.image.height
99 |
100 | const quadRenderer = new QuadRenderer({
101 | width: w,
102 | height: h,
103 | type: srcTex.type,
104 | colorSpace: srcTex.colorSpace as ColorSpace,
105 | material: mat,
106 | renderer
107 | })
108 |
109 | const frameBuffers: WebGLRenderTarget[] = []
110 |
111 | while (w > 1 || h > 1) {
112 | w = Math.max(1, (w + cellSize - 1) / cellSize | 0)
113 | h = Math.max(1, (h + cellSize - 1) / cellSize | 0)
114 | const fb = new WebGLRenderTarget(w, h, {
115 | type: quadRenderer.type,
116 | format: srcTex.format,
117 | colorSpace: quadRenderer.colorSpace,
118 | minFilter: NearestFilter,
119 | magFilter: NearestFilter,
120 | wrapS: ClampToEdgeWrapping,
121 | wrapT: ClampToEdgeWrapping,
122 | generateMipmaps: false,
123 | depthBuffer: false,
124 | stencilBuffer: false
125 | })
126 | frameBuffers.push(fb)
127 | }
128 |
129 | w = srcTex.image.width
130 | h = srcTex.image.height
131 | frameBuffers.forEach((fbi) => {
132 | w = Math.max(1, (w + cellSize - 1) / cellSize | 0)
133 | h = Math.max(1, (h + cellSize - 1) / cellSize | 0)
134 |
135 | quadRenderer.renderTarget = fbi
136 | quadRenderer.render()
137 |
138 | mat.uniforms.map.value = fbi.texture
139 | // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
140 | mat.uniforms.u_srcResolution.value.x = w
141 | // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
142 | mat.uniforms.u_srcResolution.value.y = h
143 | })
144 |
145 | const out = quadRenderer.toArray()
146 |
147 | quadRenderer.dispose()
148 | frameBuffers.forEach(fb => fb.dispose())
149 |
150 | return [
151 | quadRenderer.type === FloatType ? out[0] : DataUtils.fromHalfFloat(out[0]),
152 | quadRenderer.type === FloatType ? out[1] : DataUtils.fromHalfFloat(out[1]),
153 | quadRenderer.type === FloatType ? out[2] : DataUtils.fromHalfFloat(out[2])
154 | ]
155 | }
156 |
--------------------------------------------------------------------------------
/src/encode/get-gainmap.ts:
--------------------------------------------------------------------------------
1 | import {
2 | LinearSRGBColorSpace,
3 | UnsignedByteType
4 | } from 'three'
5 |
6 | import { getDataTexture } from '../core/get-data-texture'
7 | import { QuadRenderer } from '../core/QuadRenderer'
8 | import { GainMapEncoderMaterial } from './materials/GainMapEncoderMaterial'
9 | import { EncodingParametersBase } from './types'
10 | /**
11 | *
12 | * @param params
13 | * @returns
14 | * @category Encoding Functions
15 | * @group Encoding Functions
16 | */
17 | export const getGainMap = (params: { sdr: InstanceType } & EncodingParametersBase) => {
18 | const { image, sdr, renderer } = params
19 |
20 | const dataTexture = getDataTexture(image)
21 |
22 | const material = new GainMapEncoderMaterial({
23 | ...params,
24 | sdr: sdr.renderTarget.texture,
25 | hdr: dataTexture
26 | })
27 |
28 | const quadRenderer = new QuadRenderer({
29 | width: dataTexture.image.width,
30 | height: dataTexture.image.height,
31 | type: UnsignedByteType,
32 | colorSpace: LinearSRGBColorSpace,
33 | material,
34 | renderer,
35 | renderTargetOptions: params.renderTargetOptions
36 | })
37 | try {
38 | quadRenderer.render()
39 | } catch (e) {
40 | quadRenderer.disposeOnDemandRenderer()
41 | throw e
42 | }
43 | return quadRenderer
44 | }
45 |
--------------------------------------------------------------------------------
/src/encode/get-sdr-rendition.ts:
--------------------------------------------------------------------------------
1 | import {
2 | DataTexture,
3 | SRGBColorSpace,
4 | ToneMapping,
5 | UnsignedByteType,
6 | WebGLRenderer
7 | } from 'three'
8 |
9 | import { QuadRenderer } from '../core/QuadRenderer'
10 | import { QuadRendererTextureOptions } from '../decode'
11 | import { SDRMaterial } from './materials/SDRMaterial'
12 |
13 | /**
14 | * Renders an SDR Representation of an HDR Image
15 | *
16 | * @category Encoding Functions
17 | * @group Encoding Functions
18 | *
19 | * @param hdrTexture The HDR image to be rendered
20 | * @param renderer (optional) WebGLRenderer to use during the rendering, a disposable renderer will be create and destroyed if this is not provided.
21 | * @param toneMapping (optional) Tone mapping to be applied to the SDR Rendition
22 | * @param renderTargetOptions (optional) Options to use when creating the output renderTarget
23 | * @throws {Error} if the WebGLRenderer fails to render the SDR image
24 | */
25 | export const getSDRRendition = (hdrTexture: DataTexture, renderer?: WebGLRenderer, toneMapping?: ToneMapping, renderTargetOptions?: QuadRendererTextureOptions): InstanceType>> => {
26 | hdrTexture.needsUpdate = true
27 | const quadRenderer = new QuadRenderer({
28 | width: hdrTexture.image.width,
29 | height: hdrTexture.image.height,
30 | type: UnsignedByteType,
31 | colorSpace: SRGBColorSpace,
32 | material: new SDRMaterial({ map: hdrTexture, toneMapping }),
33 | renderer,
34 | renderTargetOptions
35 | })
36 | try {
37 | quadRenderer.render()
38 | } catch (e) {
39 | quadRenderer.disposeOnDemandRenderer()
40 | throw e
41 | }
42 | return quadRenderer
43 | }
44 |
--------------------------------------------------------------------------------
/src/encode/index.ts:
--------------------------------------------------------------------------------
1 | export * from '../core/types'
2 | export * from './compress'
3 | export * from './encode'
4 | export * from './encode-and-compress'
5 | export * from './find-texture-min-max'
6 | export * from './get-gainmap'
7 | export * from './get-sdr-rendition'
8 | export * from './materials/GainMapEncoderMaterial'
9 | export * from './materials/SDRMaterial'
10 | export * from './types'
11 |
--------------------------------------------------------------------------------
/src/encode/materials/GainMapEncoderMaterial.ts:
--------------------------------------------------------------------------------
1 | import { NoBlending, ShaderMaterial, Texture, Vector3 } from 'three'
2 |
3 | // eslint-disable-next-line unused-imports/no-unused-imports
4 | import { GainMapMetadata } from '../../core/types' // needed for docs
5 | import { GainmapEncodingParameters } from '../types'
6 |
7 | const vertexShader = /* glsl */`
8 | varying vec2 vUv;
9 |
10 | void main() {
11 | vUv = uv;
12 | gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
13 | }
14 | `
15 |
16 | const fragmentShader = /* glsl */`
17 | #ifndef saturate
18 | #define saturate( a ) clamp( a, 0.0, 1.0 )
19 | #endif
20 | uniform sampler2D sdr;
21 | uniform sampler2D hdr;
22 | uniform vec3 gamma;
23 | uniform vec3 offsetSdr;
24 | uniform vec3 offsetHdr;
25 | uniform float minLog2;
26 | uniform float maxLog2;
27 |
28 | varying vec2 vUv;
29 |
30 | void main() {
31 | vec3 sdrColor = texture2D(sdr, vUv).rgb;
32 | vec3 hdrColor = texture2D(hdr, vUv).rgb;
33 |
34 | vec3 pixelGain = (hdrColor + offsetHdr) / (sdrColor + offsetSdr);
35 | vec3 logRecovery = (log2(pixelGain) - minLog2) / (maxLog2 - minLog2);
36 | vec3 clampedRecovery = saturate(logRecovery);
37 | gl_FragColor = vec4(pow(clampedRecovery, gamma), 1.0);
38 | }
39 | `
40 | /**
41 | * A Material which is able to encode a gainmap
42 | *
43 | * @category Materials
44 | * @group Materials
45 | */
46 | export class GainMapEncoderMaterial extends ShaderMaterial {
47 | private _minContentBoost: number
48 | private _maxContentBoost: number
49 | private _offsetSdr: [number, number, number]
50 | private _offsetHdr: [number, number, number]
51 | private _gamma: [number, number, number]
52 | /**
53 | *
54 | * @param params
55 | */
56 | constructor ({ sdr, hdr, offsetSdr, offsetHdr, maxContentBoost, minContentBoost, gamma }: { sdr: Texture, hdr: Texture } & GainmapEncodingParameters) {
57 | if (!maxContentBoost) throw new Error('maxContentBoost is required')
58 | if (!sdr) throw new Error('sdr is required')
59 | if (!hdr) throw new Error('hdr is required')
60 |
61 | const _gamma = gamma || [1, 1, 1]
62 | const _offsetSdr = offsetSdr || [1 / 64, 1 / 64, 1 / 64]
63 | const _offsetHdr = offsetHdr || [1 / 64, 1 / 64, 1 / 64]
64 | const _minContentBoost = minContentBoost || 1
65 | const _maxContentBoost = Math.max(maxContentBoost, 1.0001)
66 |
67 | super({
68 | name: 'GainMapEncoderMaterial',
69 | vertexShader,
70 | fragmentShader,
71 | uniforms: {
72 | sdr: { value: sdr },
73 | hdr: { value: hdr },
74 | gamma: { value: new Vector3().fromArray(_gamma) },
75 | offsetSdr: { value: new Vector3().fromArray(_offsetSdr) },
76 | offsetHdr: { value: new Vector3().fromArray(_offsetHdr) },
77 | minLog2: { value: Math.log2(_minContentBoost) },
78 | maxLog2: { value: Math.log2(_maxContentBoost) }
79 | },
80 | blending: NoBlending,
81 | depthTest: false,
82 | depthWrite: false
83 | })
84 |
85 | this._minContentBoost = _minContentBoost
86 | this._maxContentBoost = _maxContentBoost
87 | this._offsetSdr = _offsetSdr
88 | this._offsetHdr = _offsetHdr
89 | this._gamma = _gamma
90 |
91 | this.needsUpdate = true
92 | this.uniformsNeedUpdate = true
93 | }
94 |
95 | /**
96 | * @see {@link GainmapEncodingParameters.gamma}
97 | */
98 | get gamma () { return this._gamma }
99 | set gamma (value: [number, number, number]) {
100 | this._gamma = value
101 | this.uniforms.gamma.value = new Vector3().fromArray(value)
102 | }
103 |
104 | /**
105 | * @see {@link GainmapEncodingParameters.offsetHdr}
106 | */
107 | get offsetHdr () { return this._offsetHdr }
108 | set offsetHdr (value: [number, number, number]) {
109 | this._offsetHdr = value
110 | this.uniforms.offsetHdr.value = new Vector3().fromArray(value)
111 | }
112 |
113 | /**
114 | * @see {@link GainmapEncodingParameters.offsetSdr}
115 | */
116 | get offsetSdr () { return this._offsetSdr }
117 | set offsetSdr (value: [number, number, number]) {
118 | this._offsetSdr = value
119 | this.uniforms.offsetSdr.value = new Vector3().fromArray(value)
120 | }
121 |
122 | /**
123 | * @see {@link GainmapEncodingParameters.minContentBoost}
124 | * @remarks Non logarithmic space
125 | */
126 | get minContentBoost () { return this._minContentBoost }
127 | set minContentBoost (value: number) {
128 | this._minContentBoost = value
129 | this.uniforms.minLog2.value = Math.log2(value)
130 | }
131 |
132 | /**
133 | * @see {@link GainmapEncodingParameters.maxContentBoost}
134 | * @remarks Non logarithmic space
135 | */
136 | get maxContentBoost () { return this._maxContentBoost }
137 | set maxContentBoost (value: number) {
138 | this._maxContentBoost = value
139 | this.uniforms.maxLog2.value = Math.log2(value)
140 | }
141 |
142 | /**
143 | * @see {@link GainMapMetadata.gainMapMin}
144 | * @remarks Logarithmic space
145 | */
146 | get gainMapMin (): [number, number, number] { return [Math.log2(this._minContentBoost), Math.log2(this._minContentBoost), Math.log2(this._minContentBoost)] }
147 | /**
148 | * @see {@link GainMapMetadata.gainMapMax}
149 | * @remarks Logarithmic space
150 | */
151 | get gainMapMax (): [number, number, number] { return [Math.log2(this._maxContentBoost), Math.log2(this._maxContentBoost), Math.log2(this._maxContentBoost)] }
152 |
153 | /**
154 | * @see {@link GainMapMetadata.hdrCapacityMin}
155 | * @remarks Logarithmic space
156 | */
157 | get hdrCapacityMin (): number { return Math.min(Math.max(0, this.gainMapMin[0]), Math.max(0, this.gainMapMin[1]), Math.max(0, this.gainMapMin[2])) }
158 | /**
159 | * @see {@link GainMapMetadata.hdrCapacityMax}
160 | * @remarks Logarithmic space
161 | */
162 | get hdrCapacityMax (): number { return Math.max(Math.max(0, this.gainMapMax[0]), Math.max(0, this.gainMapMax[1]), Math.max(0, this.gainMapMax[2])) }
163 | }
164 |
--------------------------------------------------------------------------------
/src/libultrahdr.ts:
--------------------------------------------------------------------------------
1 | export * from '../libultrahdr-wasm/build/libultrahdr'
2 | export * from './libultrahdr/decode-jpeg-metadata'
3 | export * from './libultrahdr/encode-jpeg-metadata'
4 | export * from './libultrahdr/library'
5 |
--------------------------------------------------------------------------------
/src/libultrahdr/decode-jpeg-metadata.ts:
--------------------------------------------------------------------------------
1 | import { GainMapMetadata } from '../core/types'
2 | import { getLibrary } from './library'
3 |
4 | /**
5 | * Decodes a JPEG file with an embedded Gainmap and XMP Metadata (aka JPEG-R)
6 | *
7 | * @category Decoding
8 | * @group Decoding
9 | * @deprecated
10 | * @example
11 | * import { decodeJPEGMetadata } from '@monogrid/gainmap-js/libultrahdr'
12 | *
13 | * // fetch a JPEG image containing a gainmap as ArrayBuffer
14 | * const gainmap = new Uint8Array(await (await fetch('gainmap.jpeg')).arrayBuffer())
15 | *
16 | * // extract data from the JPEG
17 | * const { gainMap, sdr, parsedMetadata } = await decodeJPEGMetadata(gainmap)
18 | *
19 | * @param file A Jpeg file Uint8Array.
20 | * @returns The decoded data
21 | * @throws {Error} if the provided file cannot be parsed or does not contain a valid Gainmap
22 | */
23 | /* istanbul ignore next */
24 | export const decodeJPEGMetadata = async (file: Uint8Array) => {
25 | const lib = await getLibrary()
26 | const result = lib.extractJpegR(file, file.length)
27 | if (!result.success) throw new Error(`${result.errorMessage}`)
28 |
29 | const getXMLValue = (xml: string, tag: string, defaultValue?: string): string | [string, string, string] => {
30 | // Check for attribute format first: tag="value"
31 | const attributeMatch = new RegExp(`${tag}="([^"]*)"`, 'i').exec(xml)
32 | if (attributeMatch) return attributeMatch[1]
33 |
34 | // Check for tag format: value or value...
35 | const tagMatch = new RegExp(`<${tag}[^>]*>([\\s\\S]*?)${tag}>`, 'i').exec(xml)
36 | if (tagMatch) {
37 | // Check if it contains rdf:li elements
38 | const liValues = tagMatch[1].match(/([^<]*)<\/rdf:li>/g)
39 | if (liValues && liValues.length === 3) {
40 | return liValues.map(v => v.replace(/<\/?rdf:li>/g, '')) as [string, string, string]
41 | }
42 | return tagMatch[1].trim()
43 | }
44 |
45 | if (defaultValue !== undefined) return defaultValue
46 | throw new Error(`Can't find ${tag} in gainmap metadata`)
47 | }
48 |
49 | const metadata = result.metadata as string
50 |
51 | const gainMapMin = getXMLValue(metadata, 'hdrgm:GainMapMin', '0')
52 | const gainMapMax = getXMLValue(metadata, 'hdrgm:GainMapMax')
53 | const gamma = getXMLValue(metadata, 'hdrgm:Gamma', '1')
54 | const offsetSDR = getXMLValue(metadata, 'hdrgm:OffsetSDR', '0.015625')
55 | const offsetHDR = getXMLValue(metadata, 'hdrgm:OffsetHDR', '0.015625')
56 |
57 | // These are always attributes, so we can use a simpler regex
58 | const hdrCapacityMinMatch = /hdrgm:HDRCapacityMin="([^"]*)"/.exec(metadata)
59 | const hdrCapacityMin = hdrCapacityMinMatch ? hdrCapacityMinMatch[1] : '0'
60 |
61 | const hdrCapacityMaxMatch = /hdrgm:HDRCapacityMax="([^"]*)"/.exec(metadata)
62 | if (!hdrCapacityMaxMatch) throw new Error('Incomplete gainmap metadata')
63 | const hdrCapacityMax = hdrCapacityMaxMatch[1]
64 |
65 | const parsedMetadata: GainMapMetadata = {
66 | gainMapMin: Array.isArray(gainMapMin) ? gainMapMin.map(v => parseFloat(v)) as [number, number, number] : [parseFloat(gainMapMin), parseFloat(gainMapMin), parseFloat(gainMapMin)],
67 | gainMapMax: Array.isArray(gainMapMax) ? gainMapMax.map(v => parseFloat(v)) as [number, number, number] : [parseFloat(gainMapMax), parseFloat(gainMapMax), parseFloat(gainMapMax)],
68 | gamma: Array.isArray(gamma) ? gamma.map(v => parseFloat(v)) as [number, number, number] : [parseFloat(gamma), parseFloat(gamma), parseFloat(gamma)],
69 | offsetSdr: Array.isArray(offsetSDR) ? offsetSDR.map(v => parseFloat(v)) as [number, number, number] : [parseFloat(offsetSDR), parseFloat(offsetSDR), parseFloat(offsetSDR)],
70 | offsetHdr: Array.isArray(offsetHDR) ? offsetHDR.map(v => parseFloat(v)) as [number, number, number] : [parseFloat(offsetHDR), parseFloat(offsetHDR), parseFloat(offsetHDR)],
71 | hdrCapacityMin: parseFloat(hdrCapacityMin),
72 | hdrCapacityMax: parseFloat(hdrCapacityMax)
73 | }
74 |
75 | return {
76 | ...result,
77 | parsedMetadata
78 | }
79 | }
80 |
--------------------------------------------------------------------------------
/src/libultrahdr/encode-jpeg-metadata.ts:
--------------------------------------------------------------------------------
1 | import { type GainMapMetadata } from '../core/types'
2 | import { type CompressedImage } from '../encode/types'
3 | import { getLibrary } from './library'
4 |
5 | /**
6 | * Encapsulates a Gainmap into a single JPEG file (aka: JPEG-R) with the base map
7 | * as the sdr visualization and the gainMap encoded into a MPF (Multi-Picture Format) tag.
8 | *
9 | * @category Encoding
10 | * @group Encoding
11 | *
12 | * @example
13 | * import { compress, encode, findTextureMinMax } from '@monogrid/gainmap-js'
14 | * import { encodeJPEGMetadata } from '@monogrid/gainmap-js/libultrahdr'
15 | * import { EXRLoader } from 'three/examples/jsm/loaders/EXRLoader.js'
16 | *
17 | * // load an HDR file
18 | * const loader = new EXRLoader()
19 | * const image = await loader.loadAsync('image.exr')
20 | *
21 | * // find RAW RGB Max value of a texture
22 | * const textureMax = await findTextureMinMax(image)
23 | *
24 | * // Encode the gainmap
25 | * const encodingResult = encode({
26 | * image,
27 | * maxContentBoost: Math.max.apply(this, textureMax)
28 | * })
29 | *
30 | * // obtain the RAW RGBA SDR buffer and create an ImageData
31 | * const sdrImageData = new ImageData(
32 | * encodingResult.sdr.toArray(),
33 | * encodingResult.sdr.width,
34 | * encodingResult.sdr.height
35 | * )
36 | * // obtain the RAW RGBA Gain map buffer and create an ImageData
37 | * const gainMapImageData = new ImageData(
38 | * encodingResult.gainMap.toArray(),
39 | * encodingResult.gainMap.width,
40 | * encodingResult.gainMap.height
41 | * )
42 | *
43 | * // parallel compress the RAW buffers into the specified mimeType
44 | * const mimeType = 'image/jpeg'
45 | * const quality = 0.9
46 | *
47 | * const [sdr, gainMap] = await Promise.all([
48 | * compress({
49 | * source: sdrImageData,
50 | * mimeType,
51 | * quality,
52 | * flipY: true // output needs to be flipped
53 | * }),
54 | * compress({
55 | * source: gainMapImageData,
56 | * mimeType,
57 | * quality,
58 | * flipY: true // output needs to be flipped
59 | * })
60 | * ])
61 | *
62 | * // obtain the metadata which will be embedded into
63 | * // and XMP tag inside the final JPEG file
64 | * const metadata = encodingResult.getMetadata()
65 | *
66 | * // embed the compressed images + metadata into a single
67 | * // JPEG file
68 | * const jpeg = await encodeJPEGMetadata({
69 | * ...encodingResult,
70 | * ...metadata,
71 | * sdr,
72 | * gainMap
73 | * })
74 | *
75 | * // `jpeg` will be an `Uint8Array` which can be saved somewhere
76 | *
77 | *
78 | * @param encodingResult
79 | * @returns an Uint8Array representing a JPEG-R file
80 | * @throws {Error} If `encodingResult.sdr.mimeType !== 'image/jpeg'`
81 | * @throws {Error} If `encodingResult.gainMap.mimeType !== 'image/jpeg'`
82 | */
83 | export const encodeJPEGMetadata = async (encodingResult: GainMapMetadata & { sdr: CompressedImage, gainMap: CompressedImage }) => {
84 | const lib = await getLibrary()
85 |
86 | if (encodingResult.sdr.mimeType !== 'image/jpeg') throw new Error('This function expects an SDR image compressed in jpeg')
87 | if (encodingResult.gainMap.mimeType !== 'image/jpeg') throw new Error('This function expects a GainMap image compressed in jpeg')
88 |
89 | return lib.appendGainMap(
90 | encodingResult.sdr.width, encodingResult.sdr.height,
91 | encodingResult.sdr.data, encodingResult.sdr.data.length,
92 | encodingResult.gainMap.data, encodingResult.gainMap.data.length,
93 | encodingResult.gainMapMax.reduce((p, n) => p + n, 0) / encodingResult.gainMapMax.length,
94 | encodingResult.gainMapMin.reduce((p, n) => p + n, 0) / encodingResult.gainMapMin.length,
95 | encodingResult.gamma.reduce((p, n) => p + n, 0) / encodingResult.gamma.length,
96 | encodingResult.offsetSdr.reduce((p, n) => p + n, 0) / encodingResult.offsetSdr.length,
97 | encodingResult.offsetHdr.reduce((p, n) => p + n, 0) / encodingResult.offsetHdr.length,
98 | encodingResult.hdrCapacityMin,
99 | encodingResult.hdrCapacityMax
100 | ) as Uint8Array
101 | }
102 |
--------------------------------------------------------------------------------
/src/libultrahdr/library.ts:
--------------------------------------------------------------------------------
1 | import { MainModule } from '../../libultrahdr-wasm/build/libultrahdr'
2 | // @ts-expect-error untyped
3 | import libultrahdr from '../../libultrahdr-wasm/build/libultrahdr-esm'
4 |
5 | let library: MainModule | undefined
6 |
7 | /**
8 | * Instances the WASM module and returns it, only one module will be created upon multiple calls.
9 | * @category WASM
10 | * @group WASM
11 | *
12 | * @returns
13 | */
14 | export const getLibrary = async () => {
15 | if (!library) {
16 | // eslint-disable-next-line @typescript-eslint/no-unsafe-call
17 | library = await libultrahdr() as MainModule
18 | }
19 | return library
20 | }
21 |
--------------------------------------------------------------------------------
/src/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "strict": true,
4 | "strictNullChecks": true,
5 | "strictFunctionTypes": true,
6 | "noErrorTruncation": true,
7 | "noImplicitAny": true,
8 | "noImplicitThis": true,
9 | "skipLibCheck": true,
10 |
11 | "target": "ES2018",
12 | "lib": ["ES2018", "DOM"],
13 | "noEmit": true
14 | }
15 | }
16 |
--------------------------------------------------------------------------------
/src/worker-interface.ts:
--------------------------------------------------------------------------------
1 | // @ts-expect-error untyped lib
2 | import PromiseWorker from 'promise-worker-transferable'
3 |
4 | import { type PromiseWorkerType, type WorkerInterface, type WorkerInterfaceImplementation } from './worker-types'
5 |
6 | export * from './worker-types'
7 | /**
8 | * Wraps a Regular worker into a `PromiseWorker`
9 | *
10 | * @param worker
11 | * @returns
12 | */
13 | export const getPromiseWorker = (worker: Worker) => {
14 | // eslint-disable-next-line @typescript-eslint/no-unsafe-call
15 | return new PromiseWorker(worker) as PromiseWorkerType
16 | }
17 | /**
18 | * Returns an interface where methods of the worker can be called by the host site
19 | *
20 | * @example
21 | * // this assumes a vite-like bundler understands the `?worker` import
22 | * import GainMapWorker from '@monogrid/gainmap-js/worker?worker'
23 | * import { getPromiseWorker, getWorkerInterface } from '@monogrid/gainmap-js/worker-interface'
24 | *
25 | * // turn our Worker into a PromiseWorker
26 | * const promiseWorker = getPromiseWorker(new GainMapWorker())
27 | * // get the interface
28 | * const workerInterface = getWorkerInterface(promiseWorker)
29 | *
30 | * @param worker
31 | * @returns
32 | */
33 | export const getWorkerInterface = (worker: PromiseWorkerType): WorkerInterfaceImplementation => {
34 | return {
35 | compress: (payload: WorkerInterface['compress']['request']['payload']) => worker.postMessage({ type: 'compress', payload } as WorkerInterface['compress']['request'])
36 | }
37 | }
38 |
--------------------------------------------------------------------------------
/src/worker-types.ts:
--------------------------------------------------------------------------------
1 | import { CompressedImage, type CompressParameters } from './encode/types'
2 |
3 | export type WorkerInterface = {
4 | compress: {
5 | request: {
6 | type: 'compress',
7 | payload: CompressParameters
8 | }
9 | result: Awaited & { source: Uint8ClampedArray }
10 | }
11 | }
12 |
13 | /**
14 | * Transferable
15 | */
16 | export type Transferable = ArrayBufferLike | ImageBitmap
17 |
18 | export type WorkerInterfaceImplementation = {
19 | [k in keyof WorkerInterface]: (payload: WorkerInterface[k]['request']['payload']) => Promise
20 | }
21 |
22 | export type WorkerRequest = WorkerInterface[keyof WorkerInterface]['request']
23 |
24 | export type WithTransferListFunction = (payload: T, transferList: Transferable[]) => T
25 |
26 | export type PromiseWorkerType = {
27 | // eslint-disable-next-line @typescript-eslint/no-explicit-any
28 | postMessage: (message: any, transferables?: Transferable[]) => Promise
29 | }
30 |
--------------------------------------------------------------------------------
/src/worker.ts:
--------------------------------------------------------------------------------
1 | // @ts-expect-error untyped lib
2 | import registerPromiseWorker from 'promise-worker-transferable/register'
3 |
4 | import { compress } from './encode/compress'
5 | import { type WithTransferListFunction, type WorkerInterface, type WorkerRequest } from './worker-types'
6 |
7 | const _compress = async (message: WorkerInterface['compress']['request'], withTransferList: WithTransferListFunction): Promise => {
8 | const result = await compress(message.payload)
9 | return withTransferList({
10 | ...result,
11 | source: message.payload.source instanceof ImageData ? message.payload.source.data : new Uint8ClampedArray(message.payload.source)
12 | }, [result.data.buffer, message.payload.source instanceof ImageData ? message.payload.source.data.buffer : message.payload.source.buffer])
13 | }
14 |
15 | // eslint-disable-next-line @typescript-eslint/no-unsafe-call
16 | registerPromiseWorker(async (message: WorkerRequest, withTransferList: WithTransferListFunction) => {
17 | switch (message.type) {
18 | // case 'encode-gainmap-buffers':
19 | // return encodeGainmapBuffers(message, withTransferList)
20 | case 'compress':
21 | return _compress(message, withTransferList)
22 | }
23 | })
24 |
--------------------------------------------------------------------------------
/tests/__snapshots__/decode/decode.test.ts/chromium-material-values.json:
--------------------------------------------------------------------------------
1 | {"sdr":{"metadata":{"version":4.6,"type":"Texture","generator":"Texture.toJSON"},"uuid":"55327fab-b82f-4596-92f7-3477261b561a","name":"","image":"28892867-3d10-4beb-899d-055d9994648f","mapping":300,"channel":0,"repeat":[1,1],"offset":[0,0],"center":[0,0],"rotation":0,"wrap":[1001,1001],"format":1023,"internalFormat":null,"type":1009,"colorSpace":"srgb","minFilter":1008,"magFilter":1006,"anisotropy":1,"flipY":true,"generateMipmaps":true,"premultiplyAlpha":false,"unpackAlignment":4},"gainMap":{"metadata":{"version":4.6,"type":"Texture","generator":"Texture.toJSON"},"uuid":"faa11cf8-81f1-436b-92e6-62526490046a","name":"","image":"369e2641-90ff-4df5-966d-86e5eda0610f","mapping":300,"channel":0,"repeat":[1,1],"offset":[0,0],"center":[0,0],"rotation":0,"wrap":[1001,1001],"format":1023,"internalFormat":null,"type":1009,"colorSpace":"srgb-linear","minFilter":1008,"magFilter":1006,"anisotropy":1,"flipY":true,"generateMipmaps":true,"premultiplyAlpha":false,"unpackAlignment":4},"offsetHdr":[0.015625,0.015625,0.015625],"offsetSdr":[0.015625,0.015625,0.015625],"gainMapMin":[0,0,0],"gainMapMax":[15.9993,15.9993,15.9993],"gamma":[1,1,1],"hdrCapacityMin":0,"hdrCapacityMax":15.9993,"maxDisplayBoost":65504.20944752219}
--------------------------------------------------------------------------------
/tests/__snapshots__/decode/decode.test.ts/chromium-render.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/MONOGRID/gainmap-js/fe719b9781d867b5f9487c0a13ba8eea6fac0335/tests/__snapshots__/decode/decode.test.ts/chromium-render.png
--------------------------------------------------------------------------------
/tests/__snapshots__/decode/loaders/GainMapLoader.test.ts/chromium-render.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/MONOGRID/gainmap-js/fe719b9781d867b5f9487c0a13ba8eea6fac0335/tests/__snapshots__/decode/loaders/GainMapLoader.test.ts/chromium-render.png
--------------------------------------------------------------------------------
/tests/__snapshots__/decode/loaders/HDRJPGLoader.test.ts/chromium-render-no-create-image-bitmap.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/MONOGRID/gainmap-js/fe719b9781d867b5f9487c0a13ba8eea6fac0335/tests/__snapshots__/decode/loaders/HDRJPGLoader.test.ts/chromium-render-no-create-image-bitmap.png
--------------------------------------------------------------------------------
/tests/__snapshots__/decode/loaders/HDRJPGLoader.test.ts/chromium-render-plain.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/MONOGRID/gainmap-js/fe719b9781d867b5f9487c0a13ba8eea6fac0335/tests/__snapshots__/decode/loaders/HDRJPGLoader.test.ts/chromium-render-plain.png
--------------------------------------------------------------------------------
/tests/__snapshots__/decode/loaders/HDRJPGLoader.test.ts/chromium-render.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/MONOGRID/gainmap-js/fe719b9781d867b5f9487c0a13ba8eea6fac0335/tests/__snapshots__/decode/loaders/HDRJPGLoader.test.ts/chromium-render.png
--------------------------------------------------------------------------------
/tests/__snapshots__/decode/utils/MPFExtractor.test.ts/chromium-01-jpg-gainmap.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/MONOGRID/gainmap-js/fe719b9781d867b5f9487c0a13ba8eea6fac0335/tests/__snapshots__/decode/utils/MPFExtractor.test.ts/chromium-01-jpg-gainmap.png
--------------------------------------------------------------------------------
/tests/__snapshots__/decode/utils/MPFExtractor.test.ts/chromium-01-jpg-sdr.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/MONOGRID/gainmap-js/fe719b9781d867b5f9487c0a13ba8eea6fac0335/tests/__snapshots__/decode/utils/MPFExtractor.test.ts/chromium-01-jpg-sdr.png
--------------------------------------------------------------------------------
/tests/__snapshots__/decode/utils/extractXMP.test.ts/chromium-extracts-xmp-from-01-jpg-1.txt:
--------------------------------------------------------------------------------
1 | {"gainMapMin":[-0.141127,-0.107269,-0.309696],"gainMapMax":[2.474807,2.453772,2.44652],"gamma":[0.250409,0.25,0.325208],"offsetSdr":[0.183221,0.183221,0.183221],"offsetHdr":[0.183221,0.183221,0.183221],"hdrCapacityMin":0,"hdrCapacityMax":2.8}
--------------------------------------------------------------------------------
/tests/__snapshots__/decode/utils/extractXMP.test.ts/chromium-extracts-xmp-from-02-jpg-1.txt:
--------------------------------------------------------------------------------
1 | {"gainMapMin":[0.000312,0.034719,0.053824],"gainMapMax":[3.522114,3.060263,2.706481],"gamma":[0.336811,0.357914,0.386615],"offsetSdr":[0.225551,0.225551,0.225551],"offsetHdr":[0.225551,0.225551,0.225551],"hdrCapacityMin":0,"hdrCapacityMax":4}
--------------------------------------------------------------------------------
/tests/__snapshots__/decode/utils/extractXMP.test.ts/chromium-extracts-xmp-from-03-jpg-1.txt:
--------------------------------------------------------------------------------
1 | {"gainMapMin":[0.000017,-0.000003,0.000012],"gainMapMax":[0.849514,0.748561,0.753096],"gamma":[0.25,0.25,0.25],"offsetSdr":[10.308957,10.308957,10.308957],"offsetHdr":[10.308957,10.308957,10.308957],"hdrCapacityMin":0,"hdrCapacityMax":4}
--------------------------------------------------------------------------------
/tests/__snapshots__/decode/utils/extractXMP.test.ts/chromium-extracts-xmp-from-04-jpg-1.txt:
--------------------------------------------------------------------------------
1 | {"gainMapMin":[0.023869,0.075191,0.142298],"gainMapMax":[3.527605,2.830234,1.537243],"gamma":[0.506828,0.590032,1.517708],"offsetSdr":[0.046983,0.046983,0.046983],"offsetHdr":[0.046983,0.046983,0.046983],"hdrCapacityMin":0,"hdrCapacityMax":3.9}
--------------------------------------------------------------------------------
/tests/__snapshots__/decode/utils/extractXMP.test.ts/chromium-extracts-xmp-from-05-jpg-1.txt:
--------------------------------------------------------------------------------
1 | {"gainMapMin":[-0.014377,-0.005596,-0.004277],"gainMapMax":[1.560281,1.560309,1.560328],"gamma":[0.25,0.25,0.25],"offsetSdr":[6.695587,6.695587,6.695587],"offsetHdr":[6.695587,6.695587,6.695587],"hdrCapacityMin":0,"hdrCapacityMax":4}
--------------------------------------------------------------------------------
/tests/__snapshots__/decode/utils/extractXMP.test.ts/chromium-extracts-xmp-from-06-jpg-1.txt:
--------------------------------------------------------------------------------
1 | {"gainMapMin":[-0.144892,-0.140972,-0.065712],"gainMapMax":[3.482396,3.126128,2.75434],"gamma":[0.252136,0.263644,0.25],"offsetSdr":[0.37841,0.37841,0.37841],"offsetHdr":[0.37841,0.37841,0.37841],"hdrCapacityMin":0,"hdrCapacityMax":4}
--------------------------------------------------------------------------------
/tests/__snapshots__/decode/utils/extractXMP.test.ts/chromium-extracts-xmp-from-07-jpg-1.txt:
--------------------------------------------------------------------------------
1 | {"gainMapMin":[-0.950004,-1.06685,-1.203979],"gainMapMax":[3.961703,3.737166,3.259504],"gamma":[0.622845,0.726034,0.869492],"offsetSdr":[0.028746,0.028746,0.028746],"offsetHdr":[0.028746,0.028746,0.028746],"hdrCapacityMin":0,"hdrCapacityMax":4}
--------------------------------------------------------------------------------
/tests/__snapshots__/decode/utils/extractXMP.test.ts/chromium-extracts-xmp-from-08-jpg-1.txt:
--------------------------------------------------------------------------------
1 | {"gainMapMin":[-0.012107,-0.028537,-0.071069],"gainMapMax":[2.668896,2.580987,2.457281],"gamma":[0.260823,0.354576,0.47235],"offsetSdr":[0.09302,0.09302,0.09302],"offsetHdr":[0.09302,0.09302,0.09302],"hdrCapacityMin":0,"hdrCapacityMax":4}
--------------------------------------------------------------------------------
/tests/__snapshots__/decode/utils/extractXMP.test.ts/chromium-extracts-xmp-from-09-jpg-1.txt:
--------------------------------------------------------------------------------
1 | {"gainMapMin":[-0.551779,-0.542884,-0.529494],"gainMapMax":[3.270427,3.225892,3.196919],"gamma":[0.42047,0.420672,0.421504],"offsetSdr":[0.017543,0.017543,0.017543],"offsetHdr":[0.017543,0.017543,0.017543],"hdrCapacityMin":0,"hdrCapacityMax":3.9}
--------------------------------------------------------------------------------
/tests/__snapshots__/decode/utils/extractXMP.test.ts/chromium-extracts-xmp-from-10-jpg-1.txt:
--------------------------------------------------------------------------------
1 | {"gainMapMin":[-0.135098,-0.133564,-0.128712],"gainMapMax":[2.941015,2.410044,1.595006],"gamma":[0.36099,0.406118,0.538082],"offsetSdr":[0.02503,0.02503,0.02503],"offsetHdr":[0.02503,0.02503,0.02503],"hdrCapacityMin":0,"hdrCapacityMax":4}
--------------------------------------------------------------------------------
/tests/__snapshots__/decode/utils/extractXMP.test.ts/chromium-extracts-xmp-from-abandoned-bakery-16k-jpg-1.txt:
--------------------------------------------------------------------------------
1 | {"gainMapMin":[0,0,0],"gainMapMax":[12.6294,12.6294,12.6294],"gamma":[1,1,1],"offsetSdr":[0.015625,0.015625,0.015625],"offsetHdr":[0.015625,0.015625,0.015625],"hdrCapacityMin":0,"hdrCapacityMax":12.6294}
--------------------------------------------------------------------------------
/tests/__snapshots__/decode/utils/extractXMP.test.ts/chromium-extracts-xmp-from-pisa-4k-jpg-1.txt:
--------------------------------------------------------------------------------
1 | {"gainMapMin":[0,0,0],"gainMapMax":[3.13443,3.13443,3.13443],"gamma":[1,1,1],"offsetSdr":[0.015625,0.015625,0.015625],"offsetHdr":[0.015625,0.015625,0.015625],"hdrCapacityMin":0,"hdrCapacityMax":3.13443}
--------------------------------------------------------------------------------
/tests/__snapshots__/decode/utils/extractXMP.test.ts/chromium-extracts-xmp-from-spruit-sunrise-4k-jpg-1.txt:
--------------------------------------------------------------------------------
1 | {"gainMapMin":[0,0,0],"gainMapMax":[15.9993,15.9993,15.9993],"gamma":[1,1,1],"offsetSdr":[0.015625,0.015625,0.015625],"offsetHdr":[0.015625,0.015625,0.015625],"hdrCapacityMin":0,"hdrCapacityMax":15.9993}
--------------------------------------------------------------------------------
/tests/__snapshots__/encode/encode-and-compress.test.ts/chromium-memorial-exr-encode-result.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/MONOGRID/gainmap-js/fe719b9781d867b5f9487c0a13ba8eea6fac0335/tests/__snapshots__/encode/encode-and-compress.test.ts/chromium-memorial-exr-encode-result.png
--------------------------------------------------------------------------------
/tests/__snapshots__/encode/encode-and-compress.test.ts/chromium-odd-sized-exr-encode-result.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/MONOGRID/gainmap-js/fe719b9781d867b5f9487c0a13ba8eea6fac0335/tests/__snapshots__/encode/encode-and-compress.test.ts/chromium-odd-sized-exr-encode-result.png
--------------------------------------------------------------------------------
/tests/__snapshots__/encode/encode.test.ts/chromium-memorial-exr-encode-result-custom-params-gainmap.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/MONOGRID/gainmap-js/fe719b9781d867b5f9487c0a13ba8eea6fac0335/tests/__snapshots__/encode/encode.test.ts/chromium-memorial-exr-encode-result-custom-params-gainmap.png
--------------------------------------------------------------------------------
/tests/__snapshots__/encode/encode.test.ts/chromium-memorial-exr-encode-result-custom-sdr-params.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/MONOGRID/gainmap-js/fe719b9781d867b5f9487c0a13ba8eea6fac0335/tests/__snapshots__/encode/encode.test.ts/chromium-memorial-exr-encode-result-custom-sdr-params.png
--------------------------------------------------------------------------------
/tests/__snapshots__/encode/encode.test.ts/chromium-memorial-exr-encode-result-gainmap.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/MONOGRID/gainmap-js/fe719b9781d867b5f9487c0a13ba8eea6fac0335/tests/__snapshots__/encode/encode.test.ts/chromium-memorial-exr-encode-result-gainmap.png
--------------------------------------------------------------------------------
/tests/__snapshots__/encode/encode.test.ts/chromium-memorial-exr-encode-result-with-tone-mapping-ACESFilmicToneMapping.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/MONOGRID/gainmap-js/fe719b9781d867b5f9487c0a13ba8eea6fac0335/tests/__snapshots__/encode/encode.test.ts/chromium-memorial-exr-encode-result-with-tone-mapping-ACESFilmicToneMapping.png
--------------------------------------------------------------------------------
/tests/__snapshots__/encode/encode.test.ts/chromium-memorial-exr-encode-result-with-tone-mapping-CineonToneMapping.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/MONOGRID/gainmap-js/fe719b9781d867b5f9487c0a13ba8eea6fac0335/tests/__snapshots__/encode/encode.test.ts/chromium-memorial-exr-encode-result-with-tone-mapping-CineonToneMapping.png
--------------------------------------------------------------------------------
/tests/__snapshots__/encode/encode.test.ts/chromium-memorial-exr-encode-result-with-tone-mapping-INVALID.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/MONOGRID/gainmap-js/fe719b9781d867b5f9487c0a13ba8eea6fac0335/tests/__snapshots__/encode/encode.test.ts/chromium-memorial-exr-encode-result-with-tone-mapping-INVALID.png
--------------------------------------------------------------------------------
/tests/__snapshots__/encode/encode.test.ts/chromium-memorial-exr-encode-result-with-tone-mapping-LinearToneMapping.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/MONOGRID/gainmap-js/fe719b9781d867b5f9487c0a13ba8eea6fac0335/tests/__snapshots__/encode/encode.test.ts/chromium-memorial-exr-encode-result-with-tone-mapping-LinearToneMapping.png
--------------------------------------------------------------------------------
/tests/__snapshots__/encode/encode.test.ts/chromium-memorial-exr-encode-result-with-tone-mapping-ReinhardToneMapping.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/MONOGRID/gainmap-js/fe719b9781d867b5f9487c0a13ba8eea6fac0335/tests/__snapshots__/encode/encode.test.ts/chromium-memorial-exr-encode-result-with-tone-mapping-ReinhardToneMapping.png
--------------------------------------------------------------------------------
/tests/__snapshots__/encode/encode.test.ts/chromium-memorial-exr-encode-result.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/MONOGRID/gainmap-js/fe719b9781d867b5f9487c0a13ba8eea6fac0335/tests/__snapshots__/encode/encode.test.ts/chromium-memorial-exr-encode-result.png
--------------------------------------------------------------------------------
/tests/__snapshots__/encode/find-texture-min-max.test.ts/chromium-finds-max-values-in-exr-1.txt:
--------------------------------------------------------------------------------
1 | [274,218,126]
--------------------------------------------------------------------------------
/tests/__snapshots__/encode/find-texture-min-max.test.ts/chromium-finds-min-values-in-exr-1.txt:
--------------------------------------------------------------------------------
1 | [0.212158203125,0.212158203125,0.212158203125]
--------------------------------------------------------------------------------
/tests/__snapshots__/examples/integrated-example.test.ts/chromium-initial.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/MONOGRID/gainmap-js/fe719b9781d867b5f9487c0a13ba8eea6fac0335/tests/__snapshots__/examples/integrated-example.test.ts/chromium-initial.png
--------------------------------------------------------------------------------
/tests/__snapshots__/examples/integrated-example.test.ts/chromium-zoomed-in.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/MONOGRID/gainmap-js/fe719b9781d867b5f9487c0a13ba8eea6fac0335/tests/__snapshots__/examples/integrated-example.test.ts/chromium-zoomed-in.png
--------------------------------------------------------------------------------
/tests/__snapshots__/examples/integrated-example.test.ts/chromium-zoomed-out-from-above.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/MONOGRID/gainmap-js/fe719b9781d867b5f9487c0a13ba8eea6fac0335/tests/__snapshots__/examples/integrated-example.test.ts/chromium-zoomed-out-from-above.png
--------------------------------------------------------------------------------
/tests/coverage-comment-template.md:
--------------------------------------------------------------------------------
1 | ## Coverage Report
2 |
3 | Commit: [{{short_commit_sha}}]({{commit_link}})
4 | Base: [{{base_ref}}@{{base_short_commit_sha}}]({{base_commit_link}})
5 |
6 | | Type | Base | This PR |
7 | |---------------------------|--------------------------------------------|------------------------------------------------------------------------------------|
8 | | Total Statements Coverage | {{base_total_statements_coverage_percent}} | {{total_statements_coverage_percent}} ({{total_statements_coverage_percent_diff}}) |
9 | | Total Branches Coverage | {{base_total_branches_coverage_percent}} | {{total_branches_coverage_percent}} ({{total_branches_coverage_percent_diff}}) |
10 | | Total Functions Coverage | {{base_total_functions_coverage_percent}} | {{total_functions_coverage_percent}} ({{total_functions_coverage_percent_diff}}) |
11 | | Total Lines Coverage | {{base_total_lines_coverage_percent}} | {{total_lines_coverage_percent}} ({{total_lines_coverage_percent_diff}}) |
12 |
13 |
14 | Details (changed files)
15 | {{changed_files_coverage_table}}
16 |
17 |
18 | Details (all files)
19 | {{files_coverage_table}}
20 |
21 |
--------------------------------------------------------------------------------
/tests/decode/decode.test.ts:
--------------------------------------------------------------------------------
1 | import { ConsoleMessage, expect } from '@playwright/test'
2 |
3 | import { test } from '../testWithCoverage'
4 | import { decodeInBrowser } from './decode'
5 |
6 | // const matrix = [
7 | // '01.jpg',
8 | // '02.jpg',
9 | // '03.jpg',
10 | // '04.jpg',
11 | // '05.jpg',
12 | // '06.jpg',
13 | // '07.jpg',
14 | // '08.jpg',
15 | // '09.jpg',
16 | // '10.jpg',
17 | // 'pisa-4k.jpg',
18 | // 'spruit_sunrise_4k.jpg',
19 | // 'abandoned_bakery_16k.jpg'
20 | // ]
21 |
22 | // for (const testFile of matrix) {
23 |
24 | // }
25 |
26 | test('decodes from jpeg', async ({ page }) => {
27 | await page.goto('/tests/testbed.html', { waitUntil: 'networkidle' })
28 |
29 | const logs: string[] = []
30 | page.on('console', (m: ConsoleMessage) => {
31 | logs.push(m.text())
32 | })
33 |
34 | const script = page.getByTestId('script')
35 | await expect(script).toBeAttached()
36 |
37 | const result = await page.evaluate(decodeInBrowser, { file: 'files/spruit_sunrise_4k.jpg' })
38 |
39 | expect(JSON.stringify(result.materialValues)).toMatchSnapshot({ name: 'material-values.json' })
40 |
41 | // test conversion to appropriate colorspace happens
42 | expect(logs.find(m => m.match(/Gainmap Colorspace needs to be/gi))).toBeTruthy()
43 | expect(logs.find(m => m.match(/SDR Colorspace needs to be/gi))).toBeTruthy()
44 |
45 | await expect(page).toHaveScreenshot('render.png')
46 | })
47 |
--------------------------------------------------------------------------------
/tests/decode/decode.ts:
--------------------------------------------------------------------------------
1 | import * as decode from '@monogrid/gainmap-js'
2 | import * as THREE from 'three'
3 | /**
4 | * test evaluated inside browser
5 | *
6 | * @param args
7 | * @returns
8 | */
9 | export const decodeInBrowser = async (args: { file: string }) => {
10 | const renderer = new THREE.WebGLRenderer()
11 | document.body.append(renderer.domElement)
12 | renderer.setSize(window.innerWidth, window.innerHeight)
13 |
14 | // fetch a JPEG image containing a gainmap as ArrayBuffer
15 | const file = await fetch(args.file)
16 | const fileBuffer = await file.arrayBuffer()
17 | const jpeg = new Uint8Array(fileBuffer)
18 |
19 | // extract data from the JPEG
20 | const { gainMap: gainMapBuffer, sdr: sdrBuffer, metadata } = await decode.extractGainmapFromJPEG(jpeg)
21 |
22 | // create data blobs
23 | const gainMapBlob = new Blob([gainMapBuffer], { type: 'image/jpeg' })
24 | const sdrBlob = new Blob([sdrBuffer], { type: 'image/jpeg' })
25 |
26 | // create ImageBitmap data
27 | const [gainMapImageBitmap, sdrImageBitmap] = await Promise.all([
28 | createImageBitmap(gainMapBlob, { imageOrientation: 'flipY' }),
29 | createImageBitmap(sdrBlob, { imageOrientation: 'flipY' })
30 | ])
31 |
32 | const gainMap = new THREE.Texture(gainMapImageBitmap)
33 | gainMap.needsUpdate = true
34 |
35 | const sdr = new THREE.Texture(sdrImageBitmap)
36 | sdr.needsUpdate = true
37 |
38 | // restore the HDR texture
39 | const result = decode.decode({
40 | sdr,
41 | gainMap,
42 | renderer,
43 | maxDisplayBoost: Math.pow(2, metadata.hdrCapacityMax),
44 | ...metadata
45 | })
46 |
47 | const scene = new THREE.Scene()
48 |
49 | const plane = new THREE.Mesh(
50 | new THREE.PlaneGeometry(),
51 | new THREE.MeshBasicMaterial({ map: result.renderTarget.texture })
52 | )
53 | const ratio = result.width / result.height
54 | plane.scale.y = Math.min(1, 1 / ratio)
55 | plane.scale.x = Math.min(1, ratio)
56 |
57 | const camera = new THREE.OrthographicCamera(-0.5, 0.5, 0.5, -0.5)
58 | camera.position.z = 10
59 |
60 | scene.add(plane)
61 | renderer.render(scene, camera)
62 |
63 | result.dispose()
64 |
65 | return {
66 | jpeg: Array.from(jpeg),
67 | materialValues: {
68 | sdr: result.material.sdr.toJSON(),
69 | gainMap: result.material.gainMap.toJSON(),
70 | offsetHdr: result.material.offsetHdr,
71 | offsetSdr: result.material.offsetSdr,
72 | gainMapMin: result.material.gainMapMin,
73 | gainMapMax: result.material.gainMapMax,
74 | gamma: result.material.gamma,
75 | hdrCapacityMin: result.material.hdrCapacityMin,
76 | hdrCapacityMax: result.material.hdrCapacityMax,
77 | maxDisplayBoost: result.material.maxDisplayBoost
78 | }
79 | }
80 | }
81 |
--------------------------------------------------------------------------------
/tests/decode/extract.test.ts:
--------------------------------------------------------------------------------
1 | import { expect } from '@playwright/test'
2 |
3 | import { test } from '../testWithCoverage'
4 | import { extractInBrowser } from './extract'
5 |
6 | test('extracts from a valid jpeg', async ({ page }) => {
7 | await page.goto('/tests/testbed.html', { waitUntil: 'networkidle' })
8 |
9 | const script = page.getByTestId('script')
10 | await expect(script).toBeAttached()
11 |
12 | const result = await page.evaluate(extractInBrowser, { file: 'files/spruit_sunrise_4k.jpg' })
13 |
14 | expect(result).not.toBeUndefined()
15 | })
16 |
17 | test('throws from an invalid jpeg', async ({ page }) => {
18 | await page.goto('/tests/testbed.html', { waitUntil: 'networkidle' })
19 |
20 | const script = page.getByTestId('script')
21 | await expect(script).toBeAttached()
22 |
23 | const shouldThrow = async () => {
24 | await page.evaluate(extractInBrowser, { file: 'files/plain-jpeg.jpg' })
25 | }
26 | await expect(shouldThrow).rejects.toThrowError(/XMP metadata not found/)
27 | })
28 |
--------------------------------------------------------------------------------
/tests/decode/extract.ts:
--------------------------------------------------------------------------------
1 | import * as decode from '@monogrid/gainmap-js'
2 | /**
3 | * test evaluated inside browser
4 | * @param args
5 | * @returns
6 | */
7 | export const extractInBrowser = async (args: { file: string }) => {
8 | // fetch a JPEG image containing a gainmap as ArrayBuffer
9 | const file = await fetch(args.file)
10 | const fileBuffer = await file.arrayBuffer()
11 | const jpeg = new Uint8Array(fileBuffer)
12 | return decode.extractGainmapFromJPEG(jpeg)
13 | }
14 |
--------------------------------------------------------------------------------
/tests/decode/loaders/GainMapLoader.test.ts:
--------------------------------------------------------------------------------
1 | import { ConsoleMessage, expect } from '@playwright/test'
2 |
3 | import { test } from '../../testWithCoverage'
4 | import { testGainMapLoaderInBrowser } from './gainmap-loader'
5 |
6 | // const matrix = [
7 | // '01.jpg',
8 | // '02.jpg',
9 | // '03.jpg',
10 | // '04.jpg',
11 | // '05.jpg',
12 | // '06.jpg',
13 | // '07.jpg',
14 | // '08.jpg',
15 | // '09.jpg',
16 | // '10.jpg',
17 | // 'pisa-4k.jpg',
18 | // 'spruit_sunrise_4k.jpg',
19 | // 'abandoned_bakery_16k.jpg'
20 | // ]
21 |
22 | // for (const testFile of matrix) {
23 |
24 | // }
25 |
26 | test('loads from webp', async ({ page }) => {
27 | await page.goto('/tests/testbed.html', { waitUntil: 'networkidle' })
28 |
29 | const script = page.getByTestId('script')
30 | await expect(script).toBeAttached()
31 |
32 | await page.evaluate(testGainMapLoaderInBrowser, {
33 | sdr: 'files/spruit_sunrise_4k.webp',
34 | gainmap: 'files/spruit_sunrise_4k-gainmap.webp',
35 | metadata: 'files/spruit_sunrise_4k.json'
36 | })
37 |
38 | await expect(page).toHaveScreenshot('render.png')
39 | })
40 |
41 | test('loads from webp sync', async ({ page }) => {
42 | await page.goto('/tests/testbed.html', { waitUntil: 'networkidle' })
43 |
44 | const logs: string[] = []
45 | page.on('console', (m: ConsoleMessage) => {
46 | logs.push(m.text())
47 | })
48 |
49 | const script = page.getByTestId('script')
50 | await expect(script).toBeAttached()
51 |
52 | await page.evaluate(testGainMapLoaderInBrowser, {
53 | sync: true,
54 | sdr: 'files/spruit_sunrise_4k.webp',
55 | gainmap: 'files/spruit_sunrise_4k-gainmap.webp',
56 | metadata: 'files/spruit_sunrise_4k.json'
57 | })
58 |
59 | // test loading progress happens
60 | expect(logs.find(mess => mess.match(/loading/gi))).toBeTruthy()
61 |
62 | await expect(page).toHaveScreenshot('render.png')
63 | })
64 |
65 | test('throws with an invalid sdr', async ({ page }) => {
66 | await page.goto('/tests/testbed.html', { waitUntil: 'networkidle' })
67 |
68 | const script = page.getByTestId('script')
69 | await expect(script).toBeAttached()
70 |
71 | const shouldThrow = async () => {
72 | await page.evaluate(testGainMapLoaderInBrowser, {
73 | sdr: 'files/invalid_image.png',
74 | gainmap: 'files/spruit_sunrise_4k-gainmap.webp',
75 | metadata: 'files/spruit_sunrise_4k.json'
76 | })
77 | }
78 |
79 | await expect(shouldThrow).rejects.toThrow(/The source image could not be decoded/)
80 | })
81 |
82 | test('throws with an invalid gainmap', async ({ page }) => {
83 | await page.goto('/tests/testbed.html', { waitUntil: 'networkidle' })
84 |
85 | const script = page.getByTestId('script')
86 | await expect(script).toBeAttached()
87 |
88 | const shouldThrow = async () => {
89 | await page.evaluate(testGainMapLoaderInBrowser, {
90 | sdr: 'files/spruit_sunrise_4k.webp',
91 | gainmap: 'files/invalid_image.png',
92 | metadata: 'files/spruit_sunrise_4k.json'
93 | })
94 | }
95 |
96 | await expect(shouldThrow).rejects.toThrow(/The source image could not be decoded/)
97 | })
98 |
99 | test('throws with it doesn\'t find the sdr', async ({ page }) => {
100 | await page.goto('/tests/testbed.html', { waitUntil: 'networkidle' })
101 |
102 | const script = page.getByTestId('script')
103 | await expect(script).toBeAttached()
104 |
105 | const shouldThrow = async () => {
106 | await page.evaluate(testGainMapLoaderInBrowser, {
107 | sdr: 'nope',
108 | gainmap: 'files/spruit_sunrise_4k-gainmap.webp',
109 | metadata: 'files/spruit_sunrise_4k.json'
110 | })
111 | }
112 |
113 | await expect(shouldThrow).rejects.toThrow(/404/)
114 | })
115 |
116 | test('throws with it doesn\'t find the gainmap', async ({ page }) => {
117 | await page.goto('/tests/testbed.html', { waitUntil: 'networkidle' })
118 |
119 | const script = page.getByTestId('script')
120 | await expect(script).toBeAttached()
121 |
122 | const shouldThrow = async () => {
123 | await page.evaluate(testGainMapLoaderInBrowser, {
124 | sdr: 'files/spruit_sunrise_4k.webp',
125 | gainmap: 'nope',
126 | metadata: 'files/spruit_sunrise_4k.json'
127 | })
128 | }
129 |
130 | await expect(shouldThrow).rejects.toThrow(/404/)
131 | })
132 |
133 | test('throws with it doesn\'t find the metadata', async ({ page }) => {
134 | await page.goto('/tests/testbed.html', { waitUntil: 'networkidle' })
135 |
136 | const script = page.getByTestId('script')
137 | await expect(script).toBeAttached()
138 |
139 | const shouldThrow = async () => {
140 | await page.evaluate(testGainMapLoaderInBrowser, {
141 | sdr: 'files/spruit_sunrise_4k.webp',
142 | gainmap: 'files/spruit_sunrise_4k-gainmap.webp',
143 | metadata: 'nope'
144 | })
145 | }
146 |
147 | await expect(shouldThrow).rejects.toThrow(/404/)
148 | })
149 |
--------------------------------------------------------------------------------
/tests/decode/loaders/HDRJPGLoader.test.ts:
--------------------------------------------------------------------------------
1 | import { expect } from '@playwright/test'
2 |
3 | import { disableCreateImageBitmap } from '../../disableBrowserFeatures'
4 | import { test } from '../../testWithCoverage'
5 | import { testHDRJpegLoaderInBrowser } from './hdr-jpg-loader'
6 |
7 | // const matrix = [
8 | // '01.jpg',
9 | // '02.jpg',
10 | // '03.jpg',
11 | // '04.jpg',
12 | // '05.jpg',
13 | // '06.jpg',
14 | // '07.jpg',
15 | // '08.jpg',
16 | // '09.jpg',
17 | // '10.jpg',
18 | // 'pisa-4k.jpg',
19 | // 'spruit_sunrise_4k.jpg',
20 | // 'abandoned_bakery_16k.jpg'
21 | // ]
22 |
23 | // for (const testFile of matrix) {
24 |
25 | // }
26 |
27 | test('loads from jpeg', async ({ page }) => {
28 | await page.goto('/tests/testbed.html', { waitUntil: 'networkidle' })
29 |
30 | const script = page.getByTestId('script')
31 | await expect(script).toBeAttached()
32 |
33 | await page.evaluate(testHDRJpegLoaderInBrowser, { file: 'files/spruit_sunrise_4k.jpg' })
34 |
35 | await expect(page).toHaveScreenshot('render.png')
36 | })
37 |
38 | test('loads from jpeg in browsers where createImageBitmap is not available', async ({ page }) => {
39 | await page.goto('/tests/testbed.html', { waitUntil: 'networkidle' })
40 |
41 | const script = page.getByTestId('script')
42 | await expect(script).toBeAttached()
43 |
44 | await page.evaluate(disableCreateImageBitmap)
45 | await page.evaluate(testHDRJpegLoaderInBrowser, { file: 'files/spruit_sunrise_4k.jpg' })
46 |
47 | await expect(page).toHaveScreenshot('render-no-create-image-bitmap.png')
48 | })
49 |
50 | test('loads a plain jpeg anyway', async ({ page }) => {
51 | await page.goto('/tests/testbed.html', { waitUntil: 'networkidle' })
52 |
53 | const script = page.getByTestId('script')
54 | await expect(script).toBeAttached()
55 |
56 | await page.evaluate(testHDRJpegLoaderInBrowser, { file: 'files/plain-jpeg.jpg' })
57 |
58 | await expect(page).toHaveScreenshot('render-plain.png')
59 | })
60 |
61 | test('throws with an invalid image', async ({ page }) => {
62 | await page.goto('/tests/testbed.html', { waitUntil: 'networkidle' })
63 |
64 | const script = page.getByTestId('script')
65 | await expect(script).toBeAttached()
66 |
67 | const shouldThrow = async () => {
68 | await page.evaluate(testHDRJpegLoaderInBrowser, { file: 'files/invalid_image.png' })
69 | }
70 |
71 | await expect(shouldThrow).rejects.toThrow(/The source image could not be decoded/)
72 | })
73 |
74 | test('throws with a not found image', async ({ page }) => {
75 | await page.goto('/tests/testbed.html', { waitUntil: 'networkidle' })
76 |
77 | const script = page.getByTestId('script')
78 | await expect(script).toBeAttached()
79 |
80 | const shouldThrow = async () => {
81 | await page.evaluate(testHDRJpegLoaderInBrowser, { file: 'nope' })
82 | }
83 |
84 | await expect(shouldThrow).rejects.toThrow(/404/)
85 | })
86 |
--------------------------------------------------------------------------------
/tests/decode/loaders/gainmap-loader.ts:
--------------------------------------------------------------------------------
1 | import * as decode from '@monogrid/gainmap-js'
2 | import * as THREE from 'three'
3 | /**
4 | *
5 | * @param args
6 | */
7 | export const testGainMapLoaderInBrowser = (args: { sdr: string, gainmap: string, metadata: string, exposure?: number, sync?: boolean } & Partial) => {
8 | // eslint-disable-next-line no-async-promise-executor
9 | return new Promise(async (resolve, reject) => {
10 | const renderer = new THREE.WebGLRenderer()
11 | renderer.toneMapping = THREE.LinearToneMapping
12 | renderer.toneMappingExposure = args.exposure || 1
13 |
14 | document.body.append(renderer.domElement)
15 | renderer.setSize(window.innerWidth, window.innerHeight)
16 | const loader = new decode.GainMapLoader(renderer)
17 |
18 | const onLoadingDone = (result: decode.QuadRenderer<1016, decode.GainMapDecoderMaterial>) => {
19 | if (args.maxDisplayBoost) {
20 | result.material.maxDisplayBoost = args.maxDisplayBoost
21 | result.render()
22 | }
23 |
24 | const scene = new THREE.Scene()
25 | const plane = new THREE.Mesh(
26 | new THREE.PlaneGeometry(),
27 | new THREE.MeshBasicMaterial({ map: result.renderTarget.texture })
28 | )
29 | const ratio = result.width / result.height
30 | plane.scale.y = Math.min(1, 1 / ratio)
31 | plane.scale.x = Math.min(1, ratio)
32 | scene.add(plane)
33 |
34 | scene.background = result.toDataTexture({
35 | mapping: THREE.EquirectangularReflectionMapping,
36 | minFilter: THREE.LinearFilter,
37 | generateMipmaps: false
38 | })
39 | scene.background.needsUpdate = true
40 |
41 | // result must be manually disposed
42 | // when you are done using it
43 | result.dispose()
44 |
45 | const camera = new THREE.PerspectiveCamera()
46 | camera.position.z = 3
47 | renderer.render(scene, camera)
48 |
49 | resolve()
50 | }
51 |
52 | if (!args.sync) {
53 | let result: decode.QuadRenderer<1016, decode.GainMapDecoderMaterial>
54 | try {
55 | result = await loader.loadAsync([
56 | args.sdr,
57 | args.gainmap,
58 | args.metadata
59 | ])
60 | } catch (e) {
61 | // eslint-disable-next-line @typescript-eslint/prefer-promise-reject-errors
62 | reject(e)
63 | return
64 | }
65 | onLoadingDone(result)
66 | } else {
67 | loader.load(
68 | [
69 | args.sdr,
70 | args.gainmap,
71 | args.metadata
72 | ],
73 | result => onLoadingDone(result),
74 | (evt) => {
75 | console.log('loading', evt.loaded, 'of', evt.total)
76 | }
77 | )
78 | }
79 | })
80 | }
81 |
--------------------------------------------------------------------------------
/tests/decode/loaders/hdr-jpg-loader.ts:
--------------------------------------------------------------------------------
1 | import * as decode from '@monogrid/gainmap-js'
2 | import * as THREE from 'three'
3 |
4 | export const testHDRJpegLoaderInBrowser = async (args: { file: string, exposure?: number } & Partial) => {
5 | const renderer = new THREE.WebGLRenderer()
6 | renderer.toneMapping = THREE.LinearToneMapping
7 | renderer.toneMappingExposure = args.exposure || 1
8 | document.body.append(renderer.domElement)
9 | renderer.setSize(window.innerWidth, window.innerHeight)
10 | const loader = new decode.HDRJPGLoader(renderer)
11 |
12 | const result = await loader.loadAsync(args.file)
13 |
14 | if (args.maxDisplayBoost) {
15 | result.material.maxDisplayBoost = args.maxDisplayBoost
16 | result.render()
17 | }
18 |
19 | const scene = new THREE.Scene()
20 | const plane = new THREE.Mesh(
21 | new THREE.PlaneGeometry(),
22 | new THREE.MeshBasicMaterial({ map: result.renderTarget.texture })
23 | )
24 | const ratio = result.width / result.height
25 | plane.scale.y = Math.min(1, 1 / ratio)
26 | plane.scale.x = Math.min(1, ratio)
27 | scene.add(plane)
28 |
29 | scene.background = result.toDataTexture({
30 | mapping: THREE.EquirectangularReflectionMapping,
31 | minFilter: THREE.LinearFilter,
32 | generateMipmaps: false
33 | })
34 | scene.background.needsUpdate = true
35 |
36 | // result must be manually disposed
37 | // when you are done using it
38 | result.dispose()
39 |
40 | const camera = new THREE.PerspectiveCamera()
41 | camera.position.z = 3
42 | renderer.render(scene, camera)
43 | }
44 |
--------------------------------------------------------------------------------
/tests/decode/utils/MPFExtractor.test.ts:
--------------------------------------------------------------------------------
1 | import { expect } from '@playwright/test'
2 | import sharp from 'sharp'
3 |
4 | import { test } from '../../testWithCoverage'
5 | import { testMPFExtractorInBrowser } from './MPFExtractor'
6 |
7 | const matrix = [
8 | '01.jpg'
9 | // '02.jpg',
10 | // '03.jpg',
11 | // '04.jpg',
12 | // '05.jpg',
13 | // '06.jpg',
14 | // '07.jpg',
15 | // '08.jpg',
16 | // '09.jpg',
17 | // '10.jpg',
18 | // 'pisa-4k.jpg',
19 | // 'spruit_sunrise_4k.jpg'
20 | // 'abandoned_bakery_16k.jpg' // too bit to test? snapshot testing fails
21 | ]
22 |
23 | for (const testFile of matrix) {
24 | test(`extracts gainmap from ${testFile}`, async ({ page }) => {
25 | await page.goto('/tests/testbed.html', { waitUntil: 'networkidle' })
26 |
27 | const script = page.getByTestId('script')
28 | await expect(script).toBeAttached()
29 |
30 | const result = await page.evaluate(testMPFExtractorInBrowser, `files/${testFile}`)
31 |
32 | expect(result).not.toBeNull()
33 | expect(result.length).toBe(2)
34 |
35 | const sdr = await sharp(Buffer.from(result[0]))
36 | .resize({ width: 500, height: 500, fit: 'inside' })
37 | .png({ compressionLevel: 9, effort: 10 })
38 | .toBuffer()
39 |
40 | expect(sdr).not.toBeNull() // temporary
41 | expect(sdr).toMatchSnapshot(`${testFile}-sdr.png`)
42 |
43 | const gainMap = await sharp(Buffer.from(result[1]))
44 | .resize({ width: 500, height: 500, fit: 'inside' })
45 | .png({ compressionLevel: 9, effort: 10 })
46 | .toBuffer()
47 |
48 | expect(gainMap).not.toBeNull() // temporary
49 | expect(gainMap).toMatchSnapshot(`${testFile}-gainmap.png`)
50 | })
51 | }
52 |
53 | test('throw when given a plain jpeg', async ({ page }) => {
54 | await page.goto('/tests/testbed.html', { waitUntil: 'networkidle' })
55 |
56 | const script = page.getByTestId('script')
57 | await expect(script).toBeAttached()
58 |
59 | const shouldThrow = async () => {
60 | await page.evaluate(testMPFExtractorInBrowser, 'files/plain-jpeg.jpg')
61 | }
62 |
63 | await expect(shouldThrow).rejects.toThrow(/Not a valid marker at offset/)
64 | })
65 |
66 | test('throw when given an invalid jpeg', async ({ page }) => {
67 | await page.goto('/tests/testbed.html', { waitUntil: 'networkidle' })
68 |
69 | const script = page.getByTestId('script')
70 | await expect(script).toBeAttached()
71 |
72 | const shouldThrow = async () => {
73 | await page.evaluate(testMPFExtractorInBrowser, 'files/invalid_image.png')
74 | }
75 |
76 | await expect(shouldThrow).rejects.toThrow(/Not a valid jpeg/)
77 | })
78 |
79 | // test('extracts an unrelated mpf image', async ({ page }) => {
80 | // await page.goto('/tests/testbed.html', { waitUntil: 'networkidle' })
81 |
82 | // const script = page.getByTestId('script')
83 | // await expect(script).toBeAttached()
84 |
85 | // const result = await page.evaluate(testMPFExtractorInBrowser, 'files/340_AppleiPhoneXSMax_IMG_E7156.jpg')
86 |
87 | // expect(result).not.toBeNull()
88 | // expect(result.length).toBe(2)
89 | // })
90 |
--------------------------------------------------------------------------------
/tests/decode/utils/MPFExtractor.ts:
--------------------------------------------------------------------------------
1 | import * as decode from '@monogrid/gainmap-js'
2 |
3 | /**
4 | *
5 | * @param testFile
6 | * @returns
7 | */
8 | export const testMPFExtractorInBrowser = async (testFile: string | number[]) => {
9 | let jpeg
10 | if (typeof testFile === 'string') {
11 | const file = await fetch(testFile)
12 | const fileBuffer = await file.arrayBuffer()
13 | jpeg = new Uint8Array(fileBuffer)
14 | } else {
15 | jpeg = Uint8Array.from(testFile)
16 | }
17 |
18 | const extractor = new decode.MPFExtractor({ extractFII: true, extractNonFII: true })
19 | const result = await extractor.extract(jpeg)
20 | const buffers = await Promise.all(result.map(blob => blob.arrayBuffer()))
21 | console.log(buffers)
22 | return buffers.map(buff => Array.from(new Uint8Array(buff)))
23 | }
24 |
--------------------------------------------------------------------------------
/tests/decode/utils/extractXMP.test.ts:
--------------------------------------------------------------------------------
1 | import { expect } from '@playwright/test'
2 |
3 | import { test } from '../../testWithCoverage'
4 | import { extractXMPInBrowser } from './extractXMP'
5 |
6 | const matrix = [
7 | '01.jpg',
8 | '02.jpg',
9 | '03.jpg',
10 | '04.jpg',
11 | '05.jpg',
12 | '06.jpg',
13 | '07.jpg',
14 | '08.jpg',
15 | '09.jpg',
16 | '10.jpg',
17 | 'pisa-4k.jpg',
18 | 'spruit_sunrise_4k.jpg',
19 | 'abandoned_bakery_16k.jpg'
20 | ]
21 |
22 | for (const testFile of matrix) {
23 | test(`extracts xmp from ${testFile}`, async ({ page }) => {
24 | await page.goto('/tests/testbed.html', { waitUntil: 'networkidle' })
25 |
26 | const script = page.getByTestId('script')
27 | await expect(script).toBeAttached()
28 |
29 | const result = await page.evaluate(extractXMPInBrowser, `files/${testFile}`)
30 |
31 | expect(result).not.toBeUndefined()
32 | expect(JSON.stringify(result)).toMatchSnapshot()
33 | })
34 | }
35 |
--------------------------------------------------------------------------------
/tests/decode/utils/extractXMP.ts:
--------------------------------------------------------------------------------
1 | import * as decode from '@monogrid/gainmap-js'
2 |
3 | export const extractXMPInBrowser = async (testFile: string | number[]) => {
4 | if (typeof testFile === 'string') {
5 | const file = await fetch(testFile)
6 | const fileBuffer = await file.arrayBuffer()
7 | const jpeg = new Uint8Array(fileBuffer)
8 | return decode.extractXMP(jpeg)
9 | }
10 | return decode.extractXMP(Uint8Array.from(testFile))
11 | }
12 |
--------------------------------------------------------------------------------
/tests/disableBrowserFeatures.ts:
--------------------------------------------------------------------------------
1 | export const disableCreateImageBitmap = () => {
2 | // @ts-expect-error this is intentional
3 | window.createImageBitmap = undefined
4 | }
5 | export const disableOffscreenCanvas = () => {
6 | // @ts-expect-error this is intentional
7 | window.OffscreenCanvas = undefined
8 | }
9 | export const throwOnCanvasToBlob = () => {
10 | window.HTMLCanvasElement.prototype.toBlob = function () { throw new Error('error') }
11 | }
12 | export const returnNullOnCanvasToBlob = () => {
13 | window.HTMLCanvasElement.prototype.toBlob = function (cb) { cb(null) }
14 | }
15 | export const throwOnCanvasGetContext = () => {
16 | window.HTMLCanvasElement.prototype.getContext = function () { throw new Error('error') }
17 | }
18 | export const returnNullOnCanvasGetContext = () => {
19 | window.HTMLCanvasElement.prototype.getContext = function () { return null }
20 | }
21 |
--------------------------------------------------------------------------------
/tests/encode/encode-and-compress.test.ts:
--------------------------------------------------------------------------------
1 | import { expect } from '@playwright/test'
2 | import sharp from 'sharp'
3 |
4 | import { disableCreateImageBitmap, disableOffscreenCanvas, returnNullOnCanvasToBlob } from '../disableBrowserFeatures'
5 | import { test } from '../testWithCoverage'
6 | import { encodeAndCompressInBrowser } from './encode-and-compress'
7 | // const matrix = [
8 | // '01.jpg',
9 | // '02.jpg',
10 | // '03.jpg',
11 | // '04.jpg',
12 | // '05.jpg',
13 | // '06.jpg',
14 | // '07.jpg',
15 | // '08.jpg',
16 | // '09.jpg',
17 | // '10.jpg',
18 | // 'pisa-4k.jpg',
19 | // 'spruit_sunrise_4k.jpg',
20 | // 'abandoned_bakery_16k.jpg'
21 | // ]
22 |
23 | // for (const testFile of matrix) {
24 |
25 | // }
26 |
27 | test('encodes and compresses from exr', async ({ page }) => {
28 | await page.goto('/tests/testbed.html', { waitUntil: 'networkidle' })
29 |
30 | const script = page.getByTestId('script')
31 | await expect(script).toBeAttached()
32 |
33 | const result = await page.evaluate(encodeAndCompressInBrowser, { file: 'files/memorial.exr' })
34 |
35 | expect(result.length).toBeGreaterThan(0)
36 |
37 | const resized = await sharp(Buffer.from(result))
38 | .resize({ width: 500, height: 500, fit: 'inside' })
39 | .png({ compressionLevel: 9, effort: 10 })
40 | .toBuffer()
41 |
42 | expect(resized).toMatchSnapshot('memorial.exr-encode-result.png')
43 | })
44 |
45 | test('encodes and compresses odd sized images', async ({ page }) => {
46 | await page.goto('/tests/testbed.html', { waitUntil: 'networkidle' })
47 |
48 | const script = page.getByTestId('script')
49 | await expect(script).toBeAttached()
50 |
51 | const result = await page.evaluate(encodeAndCompressInBrowser, { file: 'files/odd-sized.exr' })
52 |
53 | expect(result.length).toBeGreaterThan(0)
54 |
55 | const resized = await sharp(Buffer.from(result))
56 | .resize({ width: 500, height: 500, fit: 'inside' })
57 | .png({ compressionLevel: 9, effort: 10 })
58 | .toBuffer()
59 |
60 | expect(resized).toMatchSnapshot('odd-sized.exr-encode-result.png')
61 | })
62 |
63 | test('encodes and compresses from exr using worker', async ({ page }) => {
64 | await page.goto('/tests/testbed.html', { waitUntil: 'networkidle' })
65 |
66 | const script = page.getByTestId('script')
67 | await expect(script).toBeAttached()
68 |
69 | const result = await page.evaluate(encodeAndCompressInBrowser, { file: 'files/memorial.exr', withWorker: true })
70 |
71 | expect(result.length).toBeGreaterThan(0)
72 |
73 | const resized = await sharp(Buffer.from(result))
74 | .resize({ width: 500, height: 500, fit: 'inside' })
75 | .png({ compressionLevel: 9, effort: 10 })
76 | .toBuffer()
77 |
78 | expect(resized).toMatchSnapshot('memorial.exr-encode-result.png')
79 | })
80 |
81 | test('encodes and compresses from exr with no OffscreenCanvas', async ({ page }) => {
82 | await page.goto('/tests/testbed.html', { waitUntil: 'networkidle' })
83 |
84 | const script = page.getByTestId('script')
85 | await expect(script).toBeAttached()
86 |
87 | await page.evaluate(disableOffscreenCanvas)
88 |
89 | const result = await page.evaluate(encodeAndCompressInBrowser, { file: 'files/memorial.exr' })
90 |
91 | expect(result.length).toBeGreaterThan(0)
92 |
93 | const resized = await sharp(Buffer.from(result))
94 | .resize({ width: 500, height: 500, fit: 'inside' })
95 | .png({ compressionLevel: 9, effort: 10 })
96 | .toBuffer()
97 |
98 | expect(resized).toMatchSnapshot('memorial.exr-encode-result.png')
99 | })
100 |
101 | test('fails when canvas.toBlob return null (as in TS types possibilities)', async ({ page }) => {
102 | await page.goto('/tests/testbed.html', { waitUntil: 'networkidle' })
103 |
104 | const script = page.getByTestId('script')
105 | await expect(script).toBeAttached()
106 |
107 | await page.evaluate(disableOffscreenCanvas)
108 | await page.evaluate(returnNullOnCanvasToBlob)
109 |
110 | const shouldThrow = async () => {
111 | await page.evaluate(encodeAndCompressInBrowser, { file: 'files/memorial.exr' })
112 | }
113 |
114 | await expect(shouldThrow).rejects.toThrow(/Failed to convert canvas to blob/)
115 | })
116 |
117 | test('fails when createImageBitmap is not supported', async ({ page }) => {
118 | await page.goto('/tests/testbed.html', { waitUntil: 'networkidle' })
119 |
120 | const script = page.getByTestId('script')
121 | await expect(script).toBeAttached()
122 |
123 | await page.evaluate(disableCreateImageBitmap)
124 |
125 | const shouldThrow = async () => {
126 | await page.evaluate(encodeAndCompressInBrowser, { file: 'files/memorial.exr' })
127 | }
128 |
129 | await expect(shouldThrow).rejects.toThrow(/createImageBitmap.*not supported/gi)
130 | })
131 |
--------------------------------------------------------------------------------
/tests/encode/encode-and-compress.ts:
--------------------------------------------------------------------------------
1 | import * as encode from '@monogrid/gainmap-js/encode'
2 | import * as libultrahdr from '@monogrid/gainmap-js/libultrahdr'
3 | import * as workerInterface from '@monogrid/gainmap-js/worker-interface'
4 | import { EXRLoader } from 'three/examples/jsm/loaders/EXRLoader.js'
5 |
6 | /**
7 | * evaluated inside browser
8 | *
9 | * @param args
10 | * @returns
11 | */
12 | export const encodeAndCompressInBrowser = async (args: Omit & { file: string, maxContentBoost?: number, mimeType?: encode.CompressionMimeType, quality?: number, withWorker?: boolean }) => {
13 | let withWorker
14 | if (args.withWorker) {
15 | withWorker = workerInterface.getWorkerInterface(
16 | workerInterface.getPromiseWorker(
17 | new Worker('../dist/worker.umd.cjs')
18 | )
19 | )
20 | }
21 |
22 | // load an HDR file
23 | const image = await new EXRLoader().loadAsync(args.file)
24 |
25 | // Encode the gainmap
26 | const encodingResult = await encode.encodeAndCompress({
27 | image,
28 | toneMapping: args.toneMapping,
29 | gamma: args.gamma,
30 | minContentBoost: args.minContentBoost,
31 | offsetHdr: args.offsetHdr,
32 | offsetSdr: args.offsetSdr,
33 | renderTargetOptions: args.renderTargetOptions,
34 | maxContentBoost: args.maxContentBoost || Math.max.apply(this, encode.findTextureMinMax(image)),
35 | mimeType: args.mimeType || 'image/jpeg',
36 | quality: args.quality || 0.9,
37 | flipY: args.flipY !== undefined ? args.flipY : true,
38 | withWorker
39 | })
40 |
41 | // embed the compressed images + metadata into a single
42 | // JPEG file
43 | const jpeg = await libultrahdr.encodeJPEGMetadata({
44 | ...encodingResult,
45 | sdr: encodingResult.sdr,
46 | gainMap: encodingResult.gainMap
47 | })
48 |
49 | return Array.from(jpeg)
50 | }
51 |
--------------------------------------------------------------------------------
/tests/encode/encode.ts:
--------------------------------------------------------------------------------
1 | import * as encode from '@monogrid/gainmap-js/encode'
2 | import * as libultrahdr from '@monogrid/gainmap-js/libultrahdr'
3 | import { EXRLoader } from 'three/examples/jsm/loaders/EXRLoader.js'
4 |
5 | /**
6 | * test evaluated inside browser
7 | *
8 | * @param args
9 | * @returns
10 | */
11 | export const encodeInBrowser = async (args: Omit & { file: string, mimeType?: encode.CompressionMimeType, maxContentBoost?: number, quality?: number, dynamicGainMapParameters?: Partial, dynamicSDRParameters?: { brightness?: number, contrast?: number, saturation?: number, exposure?: number } }) => {
12 | // load an HDR file
13 | const loader = new EXRLoader()
14 | const image = await loader.loadAsync(args.file)
15 |
16 | // Encode the gainmap
17 | const encodingResult = encode.encode({
18 | image,
19 | toneMapping: args.toneMapping,
20 | gamma: args.gamma,
21 | minContentBoost: args.minContentBoost,
22 | offsetHdr: args.offsetHdr,
23 | offsetSdr: args.offsetSdr,
24 | renderTargetOptions: args.renderTargetOptions,
25 | // this will encode the full HDR range
26 | maxContentBoost: args.maxContentBoost || Math.max.apply(this, encode.findTextureMinMax(image))
27 | })
28 |
29 | // test re-render
30 | if (args.dynamicGainMapParameters !== undefined) {
31 | const { maxContentBoost, minContentBoost, gamma, offsetHdr, offsetSdr } = args.dynamicGainMapParameters
32 | if (maxContentBoost !== undefined) encodingResult.gainMap.material.maxContentBoost = maxContentBoost
33 | if (minContentBoost !== undefined) encodingResult.gainMap.material.minContentBoost = minContentBoost
34 | if (gamma !== undefined) encodingResult.gainMap.material.gamma = gamma
35 | if (offsetHdr !== undefined) encodingResult.gainMap.material.offsetHdr = offsetHdr
36 | if (offsetSdr !== undefined) encodingResult.gainMap.material.offsetSdr = offsetSdr
37 | encodingResult.gainMap.render()
38 | }
39 |
40 | // test re-render
41 | if (args.dynamicSDRParameters !== undefined) {
42 | const { brightness, contrast, exposure, saturation } = args.dynamicSDRParameters
43 | if (brightness !== undefined) encodingResult.sdr.material.brightness = brightness
44 | if (exposure !== undefined) encodingResult.sdr.material.exposure = exposure
45 | if (saturation !== undefined) encodingResult.sdr.material.saturation = saturation
46 | if (contrast !== undefined) encodingResult.sdr.material.contrast = contrast
47 | encodingResult.sdr.render()
48 | }
49 |
50 | // obtain the RAW RGBA SDR buffer and create an ImageData
51 | const sdrImageData = new ImageData(encodingResult.sdr.toArray(), encodingResult.sdr.width, encodingResult.sdr.height)
52 | // obtain the RAW RGBA Gain map buffer and create an ImageData
53 | const gainMapImageData = new ImageData(encodingResult.gainMap.toArray(), encodingResult.gainMap.width, encodingResult.gainMap.height)
54 |
55 | // parallel compress the RAW buffers into the specified mimeType
56 | const mimeType = args.mimeType || 'image/jpeg'
57 | const quality = args.quality || 0.9
58 |
59 | const [sdr, gainMap] = await Promise.all([
60 | encode.compress({
61 | source: sdrImageData,
62 | mimeType,
63 | quality,
64 | flipY: args.flipY !== undefined ? args.flipY : true // output needs to be flipped with EXR
65 | }),
66 | encode.compress({
67 | source: gainMapImageData,
68 | mimeType,
69 | quality,
70 | flipY: args.flipY !== undefined ? args.flipY : true // output needs to be flipped with EXR
71 | })
72 | ])
73 |
74 | // obtain the metadata which will be embedded into
75 | // and XMP tag inside the final JPEG file
76 | const metadata = encodingResult.getMetadata()
77 |
78 | // embed the compressed images + metadata into a single
79 | // JPEG file
80 | const jpeg = await libultrahdr.encodeJPEGMetadata({
81 | ...encodingResult,
82 | ...metadata,
83 | sdr,
84 | gainMap
85 | })
86 |
87 | encodingResult.gainMap.dispose(true)
88 | encodingResult.sdr.dispose(true)
89 |
90 | return {
91 | jpeg: Array.from(jpeg),
92 | sdrMaterialValues: {
93 | toneMapping: encodingResult.sdr.material.toneMapping,
94 | brightness: encodingResult.sdr.material.brightness,
95 | contrast: encodingResult.sdr.material.contrast,
96 | saturation: encodingResult.sdr.material.saturation
97 | },
98 | gainMapMaterialValues: {
99 | maxContentBoost: encodingResult.gainMap.material.maxContentBoost,
100 | minContentBoost: encodingResult.gainMap.material.minContentBoost,
101 | gainMapMax: encodingResult.gainMap.material.gainMapMax,
102 | gainMapMin: encodingResult.gainMap.material.gainMapMin,
103 | hdrCapacityMin: encodingResult.gainMap.material.hdrCapacityMin,
104 | hdrCapacityMax: encodingResult.gainMap.material.hdrCapacityMax,
105 | offsetHdr: encodingResult.gainMap.material.offsetHdr,
106 | offsetSdr: encodingResult.gainMap.material.offsetSdr,
107 | gamma: encodingResult.gainMap.material.gamma
108 | }
109 | }
110 | }
111 |
--------------------------------------------------------------------------------
/tests/encode/find-texture-min-max.test.ts:
--------------------------------------------------------------------------------
1 | import { expect } from '@playwright/test'
2 |
3 | import { test } from '../testWithCoverage'
4 | import { findTextureMinMaxInBrowser } from './find-texture-min-max'
5 |
6 | test('finds max values in exr', async ({ page }) => {
7 | await page.goto('/tests/testbed.html', { waitUntil: 'networkidle' })
8 |
9 | const script = page.getByTestId('script')
10 | await expect(script).toBeAttached()
11 |
12 | const result = await page.evaluate(findTextureMinMaxInBrowser, { file: 'files/memorial.exr' })
13 |
14 | expect(JSON.stringify(result)).toMatchSnapshot()
15 | })
16 |
17 | test('finds min values in exr', async ({ page }) => {
18 | await page.goto('/tests/testbed.html', { waitUntil: 'networkidle' })
19 |
20 | const script = page.getByTestId('script')
21 | await expect(script).toBeAttached()
22 |
23 | const result = await page.evaluate(findTextureMinMaxInBrowser, { file: 'files/gray.exr', mode: 'min' as const })
24 |
25 | expect(JSON.stringify(result)).toMatchSnapshot()
26 | })
27 |
--------------------------------------------------------------------------------
/tests/encode/find-texture-min-max.ts:
--------------------------------------------------------------------------------
1 | import * as encode from '@monogrid/gainmap-js/encode'
2 | import { EXRLoader } from 'three/examples/jsm/loaders/EXRLoader.js'
3 | /**
4 | *
5 | * @param file
6 | * @returns
7 | */
8 | export const findTextureMinMaxInBrowser = async (args: { file: string, mode?: 'min' | 'max' }) => {
9 | // load an HDR file
10 | const image = await new EXRLoader().loadAsync(args.file)
11 |
12 | // find RAW RGB Max value of a texture
13 | return encode.findTextureMinMax(image, args.mode)
14 | }
15 |
--------------------------------------------------------------------------------
/tests/examples/integrated-example.test.ts:
--------------------------------------------------------------------------------
1 | import { expect } from '@playwright/test'
2 | import { readFile } from 'fs/promises'
3 | import { join } from 'path'
4 |
5 | import { test } from '../testWithCoverage'
6 |
7 | // @ts-expect-error tsc throws "Named capturing groups are only available when targeting 'ES2018' or later." here
8 | const threeMatch = /https:\/\/unpkg\.com\/three(?@[0-9.]+)?\/(?.*)/
9 | // @ts-expect-error tsc throws "Named capturing groups are only available when targeting 'ES2018' or later." here
10 | const gainmapJSMatch = /https:\/\/unpkg\.com\/@monogrid\/gainmap-js(?@[0-9.]+)?\/(?.*)/
11 |
12 | test('renders the example correctly', async ({ page, context }) => {
13 | await context.route(threeMatch, async (route, request) => {
14 | const match = threeMatch.exec(request.url())
15 | const path = match?.groups?.path
16 | if (path) {
17 | const body = await readFile(join('./node_modules/three/', path))
18 | return route.fulfill({ body, status: 200, contentType: 'application/javascript' })
19 | }
20 | return route.continue()
21 | })
22 | await context.route(gainmapJSMatch, async (route, request) => {
23 | const match = gainmapJSMatch.exec(request.url())
24 | const path = match?.groups?.path
25 | if (path) {
26 | const body = await readFile(join('.', path))
27 | return route.fulfill({ body, status: 200, contentType: 'application/javascript' })
28 | }
29 | return route.continue()
30 | })
31 |
32 | await page.goto('/examples/integrated/', { waitUntil: 'networkidle' })
33 |
34 | await expect(page.locator('canvas').first()).toBeAttached()
35 |
36 | await expect(page).toHaveScreenshot('initial.png')
37 |
38 | await page.mouse.wheel(0, -9000)
39 |
40 | await expect(page).toHaveScreenshot('zoomed-in.png')
41 |
42 | await page.mouse.wheel(0, 9000)
43 |
44 | await page.mouse.move(250, 250, { steps: 20 })
45 | await page.mouse.down({ button: 'left' })
46 | await page.mouse.move(250, 500, { steps: 20 })
47 | await page.mouse.up({ button: 'left' })
48 |
49 | await expect(page).toHaveScreenshot('zoomed-out-from-above.png')
50 | })
51 |
--------------------------------------------------------------------------------
/tests/files/01.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/MONOGRID/gainmap-js/fe719b9781d867b5f9487c0a13ba8eea6fac0335/tests/files/01.jpg
--------------------------------------------------------------------------------
/tests/files/02.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/MONOGRID/gainmap-js/fe719b9781d867b5f9487c0a13ba8eea6fac0335/tests/files/02.jpg
--------------------------------------------------------------------------------
/tests/files/03.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/MONOGRID/gainmap-js/fe719b9781d867b5f9487c0a13ba8eea6fac0335/tests/files/03.jpg
--------------------------------------------------------------------------------
/tests/files/04.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/MONOGRID/gainmap-js/fe719b9781d867b5f9487c0a13ba8eea6fac0335/tests/files/04.jpg
--------------------------------------------------------------------------------
/tests/files/05.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/MONOGRID/gainmap-js/fe719b9781d867b5f9487c0a13ba8eea6fac0335/tests/files/05.jpg
--------------------------------------------------------------------------------
/tests/files/06.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/MONOGRID/gainmap-js/fe719b9781d867b5f9487c0a13ba8eea6fac0335/tests/files/06.jpg
--------------------------------------------------------------------------------
/tests/files/07.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/MONOGRID/gainmap-js/fe719b9781d867b5f9487c0a13ba8eea6fac0335/tests/files/07.jpg
--------------------------------------------------------------------------------
/tests/files/08.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/MONOGRID/gainmap-js/fe719b9781d867b5f9487c0a13ba8eea6fac0335/tests/files/08.jpg
--------------------------------------------------------------------------------
/tests/files/09.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/MONOGRID/gainmap-js/fe719b9781d867b5f9487c0a13ba8eea6fac0335/tests/files/09.jpg
--------------------------------------------------------------------------------
/tests/files/10.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/MONOGRID/gainmap-js/fe719b9781d867b5f9487c0a13ba8eea6fac0335/tests/files/10.jpg
--------------------------------------------------------------------------------
/tests/files/abandoned_bakery_16k.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/MONOGRID/gainmap-js/fe719b9781d867b5f9487c0a13ba8eea6fac0335/tests/files/abandoned_bakery_16k.jpg
--------------------------------------------------------------------------------
/tests/files/chcaus2-bloom.exr:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/MONOGRID/gainmap-js/fe719b9781d867b5f9487c0a13ba8eea6fac0335/tests/files/chcaus2-bloom.exr
--------------------------------------------------------------------------------
/tests/files/chcaus2-bloom.hdr:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/MONOGRID/gainmap-js/fe719b9781d867b5f9487c0a13ba8eea6fac0335/tests/files/chcaus2-bloom.hdr
--------------------------------------------------------------------------------
/tests/files/gray.exr:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/MONOGRID/gainmap-js/fe719b9781d867b5f9487c0a13ba8eea6fac0335/tests/files/gray.exr
--------------------------------------------------------------------------------
/tests/files/grey.exr:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/MONOGRID/gainmap-js/fe719b9781d867b5f9487c0a13ba8eea6fac0335/tests/files/grey.exr
--------------------------------------------------------------------------------
/tests/files/invalid_image.png:
--------------------------------------------------------------------------------
1 | invalid
2 |
--------------------------------------------------------------------------------
/tests/files/memorial.exr:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/MONOGRID/gainmap-js/fe719b9781d867b5f9487c0a13ba8eea6fac0335/tests/files/memorial.exr
--------------------------------------------------------------------------------
/tests/files/memorial.hdr:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/MONOGRID/gainmap-js/fe719b9781d867b5f9487c0a13ba8eea6fac0335/tests/files/memorial.hdr
--------------------------------------------------------------------------------
/tests/files/memorial.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/MONOGRID/gainmap-js/fe719b9781d867b5f9487c0a13ba8eea6fac0335/tests/files/memorial.jpg
--------------------------------------------------------------------------------
/tests/files/odd-sized.exr:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/MONOGRID/gainmap-js/fe719b9781d867b5f9487c0a13ba8eea6fac0335/tests/files/odd-sized.exr
--------------------------------------------------------------------------------
/tests/files/pisa-4k.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/MONOGRID/gainmap-js/fe719b9781d867b5f9487c0a13ba8eea6fac0335/tests/files/pisa-4k.jpg
--------------------------------------------------------------------------------
/tests/files/plain-jpeg.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/MONOGRID/gainmap-js/fe719b9781d867b5f9487c0a13ba8eea6fac0335/tests/files/plain-jpeg.jpg
--------------------------------------------------------------------------------
/tests/files/spruit_sunrise_1k.hdr:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/MONOGRID/gainmap-js/fe719b9781d867b5f9487c0a13ba8eea6fac0335/tests/files/spruit_sunrise_1k.hdr
--------------------------------------------------------------------------------
/tests/files/spruit_sunrise_4k-gainmap.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/MONOGRID/gainmap-js/fe719b9781d867b5f9487c0a13ba8eea6fac0335/tests/files/spruit_sunrise_4k-gainmap.webp
--------------------------------------------------------------------------------
/tests/files/spruit_sunrise_4k.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/MONOGRID/gainmap-js/fe719b9781d867b5f9487c0a13ba8eea6fac0335/tests/files/spruit_sunrise_4k.jpg
--------------------------------------------------------------------------------
/tests/files/spruit_sunrise_4k.json:
--------------------------------------------------------------------------------
1 | {
2 | "gainMapMax": [
3 | 15.99929538702341,
4 | 15.99929538702341,
5 | 15.99929538702341
6 | ],
7 | "gainMapMin": [
8 | 0,
9 | 0,
10 | 0
11 | ],
12 | "gamma": [
13 | 1,
14 | 1,
15 | 1
16 | ],
17 | "hdrCapacityMax": 15.99929538702341,
18 | "hdrCapacityMin": 0,
19 | "offsetHdr": [
20 | 0.015625,
21 | 0.015625,
22 | 0.015625
23 | ],
24 | "offsetSdr": [
25 | 0.015625,
26 | 0.015625,
27 | 0.015625
28 | ]
29 | }
--------------------------------------------------------------------------------
/tests/files/spruit_sunrise_4k.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/MONOGRID/gainmap-js/fe719b9781d867b5f9487c0a13ba8eea6fac0335/tests/files/spruit_sunrise_4k.webp
--------------------------------------------------------------------------------
/tests/global.d.ts:
--------------------------------------------------------------------------------
1 | export declare global {
2 | // eslint-disable-next-line unused-imports/no-unused-vars
3 | interface Window {
4 | collectIstanbulCoverage: (coverageJSON: string) => void
5 | __coverage__: Record
6 | }
7 | }
8 |
--------------------------------------------------------------------------------
/tests/testWithCoverage.ts:
--------------------------------------------------------------------------------
1 | import { test as baseTest } from '@playwright/test'
2 | import { randomBytes } from 'crypto'
3 | import { writeFileSync } from 'fs'
4 | import { mkdir } from 'fs/promises'
5 | import { join } from 'path'
6 |
7 | const istanbulCLIOutput = join(process.cwd(), '.nyc_output')
8 |
9 | export function generateUUID (): string {
10 | return randomBytes(16).toString('hex')
11 | }
12 |
13 | export const test = baseTest.extend({
14 | context: async ({ context }, use) => {
15 | await context.addInitScript(() => {
16 | /* Deterministic math random */
17 | let seed = Math.PI / 4
18 | window.Math.random = function () {
19 | // const x = window.location.href.split('').map(c => c.charCodeAt(0)).reduce((v,i) => v + i, 0)
20 | const x = Math.sin(seed++) * 10000
21 | return x - Math.floor(x)
22 | }
23 |
24 | /* Collect coverage */
25 | window.addEventListener('beforeunload', () =>
26 | window.collectIstanbulCoverage(JSON.stringify(window.__coverage__))
27 | )
28 |
29 | // @ts-expect-error injecting variables
30 | window.TESTING = true
31 |
32 | /* Deterministic Font Rendering across platforms */
33 | let styleInjected = false
34 | document.addEventListener('readystatechange', (e) => {
35 | if (!styleInjected) {
36 | styleInjected = true
37 | const style = document.createElement('style')
38 | style.innerHTML = `
39 | @import url('https://fonts.googleapis.com/css2?family=Noto+Sans&display=swap');
40 |
41 | #info, .lbl, .lil-gui, canvas[width="80"] {
42 | display: none !important;
43 | }
44 |
45 | body, html {
46 | margin: 0;
47 | padding: 0;
48 | }
49 |
50 | *, *:before, *:after {
51 | font-family: 'Noto Sans' !important;
52 | font-size: 16px !important;
53 | font-weight: 400 !important;
54 | font-kerning: none !important;
55 | font-style: normal !important;
56 | -webkit-font-smoothing: antialiased !important;
57 | text-rendering: geometricprecision !important;
58 | }`
59 | document.head.append(style)
60 | }
61 | })
62 | })
63 | await mkdir(istanbulCLIOutput, { recursive: true })
64 | await context.exposeFunction('collectIstanbulCoverage', (coverageJSON: string) => {
65 | if (coverageJSON) { writeFileSync(join(istanbulCLIOutput, `playwright_coverage_${generateUUID()}.json`), coverageJSON) }
66 | })
67 | await use(context)
68 | for (const page of context.pages()) {
69 | await page.evaluate(() => window.collectIstanbulCoverage(JSON.stringify(window.__coverage__)))
70 | }
71 | }
72 | })
73 |
74 | export const expect = test.expect
75 |
--------------------------------------------------------------------------------
/tests/testbed.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
20 |
21 |
22 |
23 |
40 |
43 |
--------------------------------------------------------------------------------
/tests/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "strict": true,
4 | "strictNullChecks": true,
5 | "strictFunctionTypes": true,
6 | "noErrorTruncation": true,
7 | "noImplicitAny": true,
8 | "noImplicitThis": true,
9 | "allowSyntheticDefaultImports": true,
10 | "esModuleInterop": true,
11 | "skipLibCheck": true,
12 | "noEmit": true,
13 | "lib": ["DOM", "ES2019"],
14 | "paths": {
15 | "three": [ "../node_modules/three/build/three.module" ],
16 | "@monogrid/gainmap-js": [ "../dist/decode" ],
17 | "@monogrid/gainmap-js/encode": [ "../dist/encode" ],
18 | "@monogrid/gainmap-js/libultrahdr": [ "../dist/libultrahdr" ],
19 | "@monogrid/gainmap-js/worker": [ "../dist/worker" ],
20 | "@monogrid/gainmap-js/worker-interface": [ "../dist/worker-interface" ]
21 | },
22 | "types": ["@playwright/test", "@types/node"]
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "ES2020",
4 | "module": "node16",
5 | "strict": true,
6 | "strictNullChecks": true,
7 | "strictFunctionTypes": true,
8 | "noErrorTruncation": true,
9 | "noImplicitAny": true,
10 | "noImplicitThis": true,
11 | "allowJs": true,
12 | "checkJs": true,
13 | "resolveJsonModule": true,
14 | "esModuleInterop": true,
15 | "noEmit": true,
16 | "skipLibCheck": true
17 | },
18 | "exclude": ["dist", "src", "libultrahdr-wasm", "tests", "examples", "coverage", "playwright-report", ".nyc_output"]
19 | }
20 |
--------------------------------------------------------------------------------
/typedoc.config.cjs:
--------------------------------------------------------------------------------
1 | /** @type {import('typedoc').TypeDocOptions} */
2 | module.exports = {
3 | entryPoints: ['./src/core/index.ts', './src/decode.ts', './src/encode.ts', './src/libultrahdr.ts', './src/worker-interface.ts'],
4 | tsconfig: './src/tsconfig.json',
5 | // entryPointStrategy: 'Merge',
6 | out: 'wiki',
7 | plugin: ['typedoc-plugin-markdown', 'typedoc-github-wiki-theme'],
8 | excludeExternals: true,
9 | excludePrivate: true,
10 | excludeInternal: true,
11 | validation: {
12 | notExported: true,
13 | invalidLink: true,
14 | notDocumented: false
15 | },
16 | treatWarningsAsErrors: true
17 | }
18 |
--------------------------------------------------------------------------------