Favivon - Correção

This commit is contained in:
2026-05-30 19:59:39 -03:00
parent 76ddaa815d
commit d7dfd221f0
32859 changed files with 5459654 additions and 404 deletions
+186
View File
@@ -0,0 +1,186 @@
# [3.4.0](https://github.com/MONOGRID/gainmap-js/compare/v3.3.0...v3.4.0) (2025-11-24)
### Features
* adds AgXToneMapping and NeutralToneMapping to encoding SDR Material shader ([8c163da](https://github.com/MONOGRID/gainmap-js/commit/8c163da5c208412814d3f4ee9e3647d0ad3bba12))
# [3.3.0](https://github.com/MONOGRID/gainmap-js/compare/v3.2.0...v3.3.0) (2025-11-22)
### Features
* removes wasm dependency and implements pure-js JPG encoder ([#73](https://github.com/MONOGRID/gainmap-js/issues/73)) ([00b6e43](https://github.com/MONOGRID/gainmap-js/commit/00b6e43f2ada89e39d76491017ff173574a20873))
# [3.2.0](https://github.com/MONOGRID/gainmap-js/compare/v3.1.0...v3.2.0) (2025-11-17)
### Features
* WebGPU support ([#72](https://github.com/MONOGRID/gainmap-js/issues/72)) ([0d85bec](https://github.com/MONOGRID/gainmap-js/commit/0d85becd0e8b79fc5adfb5bbd757e35e032ee77e))
# [3.1.0](https://github.com/MONOGRID/gainmap-js/compare/v3.0.6...v3.1.0) (2024-12-05)
### Features
* **decode:** removes `DOMParser`, decode is now compatible with Web Workers ([b6a607d](https://github.com/MONOGRID/gainmap-js/commit/b6a607d1be3e7e86524e059c9eb61c8ee2a9e6f7))
## [3.0.6](https://github.com/MONOGRID/gainmap-js/compare/v3.0.5...v3.0.6) (2024-09-03)
### Bug Fixes
* remove LogLuv following threejs removal https://github.com/mrdoob/three.js/pull/29144 ([1df2154](https://github.com/MONOGRID/gainmap-js/commit/1df2154dce59aff39439eb4974c42b55eddc862b))
## [3.0.5](https://github.com/MONOGRID/gainmap-js/compare/v3.0.4...v3.0.5) (2024-04-03)
### Bug Fixes
* **package.json:** publint fixes so that package is correctly imported in next.js ([#36](https://github.com/MONOGRID/gainmap-js/issues/36)) ([490df10](https://github.com/MONOGRID/gainmap-js/commit/490df10f401a82267595dd9e161cc6a0bbce3d94))
## [3.0.4](https://github.com/MONOGRID/gainmap-js/compare/v3.0.3...v3.0.4) (2024-04-02)
### Bug Fixes
* adds setRenderer on LoaderBase ([#34](https://github.com/MONOGRID/gainmap-js/issues/34)) ([18ad0ec](https://github.com/MONOGRID/gainmap-js/commit/18ad0eca49d95685b7f78fc4137f4aaf1338e782))
## [3.0.3](https://github.com/MONOGRID/gainmap-js/compare/v3.0.2...v3.0.3) (2024-02-09)
### Bug Fixes
* **libultrahdr-wasm:** encoding of odd sized images ([#31](https://github.com/MONOGRID/gainmap-js/issues/31)) ([efd52c3](https://github.com/MONOGRID/gainmap-js/commit/efd52c385387d2575c161e243b07369100e8e349))
## [3.0.2](https://github.com/MONOGRID/gainmap-js/compare/v3.0.1...v3.0.2) (2024-01-31)
### Bug Fixes
* **encode:** encodes a valid image when an invalid tonemapping value was provided as input ([94cec23](https://github.com/MONOGRID/gainmap-js/commit/94cec2363aa0dcb3cc46fca048c68ed7e555ebd0))
## [3.0.1](https://github.com/MONOGRID/gainmap-js/compare/v3.0.0...v3.0.1) (2023-12-03)
### Bug Fixes
* **encode:** hdrCapacityMax correctly computed in output metadata ([f5f2215](https://github.com/MONOGRID/gainmap-js/commit/f5f22152eb236831cb9f23646445e6459be18285))
* **encode:** findTextureMinMax correctly finds min values in textures ([fd94680](https://github.com/MONOGRID/gainmap-js/commit/fd94680a76fdb04b5aa85b4210f3aec21b45a727))
# [3.0.0](https://github.com/MONOGRID/gainmap-js/compare/v2.0.7...v3.0.0) (2023-11-29)
### Bug Fixes
* **loaders:** properly catches render errors and calls onError callback ([b9bcdd1](https://github.com/MONOGRID/gainmap-js/commit/b9bcdd127576fa61c6ee92c876f4845cd80b4d34)), closes [#16](https://github.com/MONOGRID/gainmap-js/issues/16)
### Features
* **core:** disables default mipmap generation, enables user to specify renderTarget (and toDataTexture) options ([147d278](https://github.com/MONOGRID/gainmap-js/commit/147d2783224cb0a2039d762abf4e4b972b0e86da)), closes [#14](https://github.com/MONOGRID/gainmap-js/issues/14) [#15](https://github.com/MONOGRID/gainmap-js/issues/15)
### BREAKING CHANGES
* **core:** `generateMipmaps` is no longer `true` by default, both `minFilter` is no longer `LinearMipMapLinearFilter` by default but `LinearFilter`, `wrapS` and `warpT` are no longer `RepeatWrapping` by default but `ClampToEdgeWrapping`
## [2.0.7](https://github.com/MONOGRID/gainmap-js/compare/v2.0.6...v2.0.7) (2023-11-23)
### Bug Fixes
* **core:** QuadRenderer dispose method now properly disposes of its internal resources ([#13](https://github.com/MONOGRID/gainmap-js/issues/13)) ([8e4473d](https://github.com/MONOGRID/gainmap-js/commit/8e4473da77732080de24f98dd271fecda06f9e53))
* **HDRJPGLoader:** renders (and returns) an SDR image when provided with a normal jpeg file ([#12](https://github.com/MONOGRID/gainmap-js/issues/12)) ([5222151](https://github.com/MONOGRID/gainmap-js/commit/5222151e6b5c95df79f1c4085cf36f30bd9c2dc4))
## [2.0.6](https://github.com/MONOGRID/gainmap-js/compare/v2.0.5...v2.0.6) (2023-11-20)
### Bug Fixes
* **decode:** changes usage of `NoColorSpace` to `LinearSRGBColorSpace` ([587dc03](https://github.com/MONOGRID/gainmap-js/commit/587dc0377876533b18b506516d8757e04e830c62))
* **loaders:** fixes LoadingManager `onLoad` actually waiting for gainmaps to be generated ([77170f5](https://github.com/MONOGRID/gainmap-js/commit/77170f57c5ad277d3e97d1236ab93a29106c4a99))
## [2.0.5](https://github.com/MONOGRID/gainmap-js/compare/v2.0.4...v2.0.5) (2023-11-16)
### Bug Fixes
* **jpegrloader:** rename JPEGRLoader to HDRJPEGLoader, old name kept for compatibility ([ac6e386](https://github.com/MONOGRID/gainmap-js/commit/ac6e38610886b7adec5806a7ad301bff1ec00d62))
## [2.0.4](https://github.com/MONOGRID/gainmap-js/compare/v2.0.3...v2.0.4) (2023-11-15)
### Bug Fixes
* **decode:** clamp max values in the decode shader to min/max half float ([96986be](https://github.com/MONOGRID/gainmap-js/commit/96986be23dd95825d19fac3b3de520d59d8ad936))
## [2.0.3](https://github.com/MONOGRID/gainmap-js/compare/v2.0.2...v2.0.3) (2023-11-15)
### Bug Fixes
* **gainmaploader:** fix GainMapLoader progress handler not being calculated correctly ([a8e556a](https://github.com/MONOGRID/gainmap-js/commit/a8e556ab1d465a9bd705960d175464cf8d434ea1))
## [2.0.2](https://github.com/MONOGRID/gainmap-js/compare/v2.0.1...v2.0.2) (2023-11-14)
### Bug Fixes
* **decode:** improves compatibility with browsers with no createImageBitmap ([12c7609](https://github.com/MONOGRID/gainmap-js/commit/12c7609ee815a460f95419aab328b412e759f012))
* **decoder:** fix bug when using decodeResult.renderTarget.texture as source for PMREMGenerator ([4ebb983](https://github.com/MONOGRID/gainmap-js/commit/4ebb983d5bbc7f524e4b1231f630e3714cf1f870))
## [2.0.1](https://github.com/MONOGRID/gainmap-js/compare/v2.0.0...v2.0.1) (2023-11-14)
### Bug Fixes
* **decode:** implements proper feature testing for QuadRenderer.toArray ([20109ad](https://github.com/MONOGRID/gainmap-js/commit/20109ad31977124c1169f096e8ccd36628599f89))
# [2.0.0](https://github.com/MONOGRID/gainmap-js/compare/v1.1.1...v2.0.0) (2023-11-13)
### Features
* removes libultrahdr wasm from the decoding part of the library, allows users to load JPEGR files using pure js ([#9](https://github.com/MONOGRID/gainmap-js/issues/9)) ([3ad16f9](https://github.com/MONOGRID/gainmap-js/commit/3ad16f97fec6040fdfdfb4cd5e71b1ac8e504e28))
### BREAKING CHANGES
* The encoder portion of the library has been separated and moved to `@monogrid/gainmap-js/encode`, in order to save file size on user's bundles, all encoding functions must now be imported with that path.
`JPEGRLoader` has been moved from `@monogrid/gainmap-js/libultrahdr` to `@monogrid/gainmap-js` because it now uses a pure js approach
## [1.1.1](https://github.com/MONOGRID/gainmap-js/compare/v1.1.0...v1.1.1) (2023-11-09)
### Bug Fixes
* fixes Firefox Compatibility ([1cec657](https://github.com/MONOGRID/gainmap-js/commit/1cec65708127b6fd064277f4f923fc0f65610fa2))
* **libultrahdr:** fixes circular dependency between JPEGRLoader and libultrahdr ([f30f786](https://github.com/MONOGRID/gainmap-js/commit/f30f7865fb27a601635168b8a104a9935653e758))
# [1.1.0](https://github.com/MONOGRID/gainmap-js/compare/v1.0.2...v1.1.0) (2023-11-06)
### Features
* **decode:** adds threejs loaders ([b32f02a](https://github.com/MONOGRID/gainmap-js/commit/b32f02a09d20fbd9d17b35c65cf4dba8aafc9ed5))
## [1.0.2](https://github.com/MONOGRID/gainmap-js/compare/v1.0.1...v1.0.2) (2023-11-03)
### Bug Fixes
* **release:** add publishConfig to package.json ([cffff0b](https://github.com/MONOGRID/gainmap-js/commit/cffff0b31050ab54040922748b82d97e6aa820a3))
## [1.0.1](https://github.com/MONOGRID/gainmap-js/compare/v1.0.0...v1.0.1) (2023-11-03)
### Bug Fixes
* **release:** scoped package for NPM publishing ([0e30758](https://github.com/MONOGRID/gainmap-js/commit/0e307589e51dd05e160062f2ae78fc746cbdf5aa))
# 1.0.0 (2023-11-03)
First Release
+128
View File
@@ -0,0 +1,128 @@
# Contributor Covenant Code of Conduct
## Our Pledge
We as members, contributors, and leaders pledge to make participation in our
community a harassment-free experience for everyone, regardless of age, body
size, visible or invisible disability, ethnicity, sex characteristics, gender
identity and expression, level of experience, education, socio-economic status,
nationality, personal appearance, race, religion, or sexual identity
and orientation.
We pledge to act and interact in ways that contribute to an open, welcoming,
diverse, inclusive, and healthy community.
## Our Standards
Examples of behavior that contributes to a positive environment for our
community include:
* Demonstrating empathy and kindness toward other people
* Being respectful of differing opinions, viewpoints, and experiences
* Giving and gracefully accepting constructive feedback
* Accepting responsibility and apologizing to those affected by our mistakes,
and learning from the experience
* Focusing on what is best not just for us as individuals, but for the
overall community
Examples of unacceptable behavior include:
* The use of sexualized language or imagery, and sexual attention or
advances of any kind
* Trolling, insulting or derogatory comments, and personal or political attacks
* Public or private harassment
* Publishing others' private information, such as a physical or email
address, without their explicit permission
* Other conduct which could reasonably be considered inappropriate in a
professional setting
## Enforcement Responsibilities
Community leaders are responsible for clarifying and enforcing our standards of
acceptable behavior and will take appropriate and fair corrective action in
response to any behavior that they deem inappropriate, threatening, offensive,
or harmful.
Community leaders have the right and responsibility to remove, edit, or reject
comments, commits, code, wiki edits, issues, and other contributions that are
not aligned to this Code of Conduct, and will communicate reasons for moderation
decisions when appropriate.
## Scope
This Code of Conduct applies within all community spaces, and also applies when
an individual is officially representing the community in public spaces.
Examples of representing our community include using an official e-mail address,
posting via an official social media account, or acting as an appointed
representative at an online or offline event.
## Enforcement
Instances of abusive, harassing, or otherwise unacceptable behavior may be
reported to the community leaders responsible for enforcement at
gainmap@monogrid.com.
All complaints will be reviewed and investigated promptly and fairly.
All community leaders are obligated to respect the privacy and security of the
reporter of any incident.
## Enforcement Guidelines
Community leaders will follow these Community Impact Guidelines in determining
the consequences for any action they deem in violation of this Code of Conduct:
### 1. Correction
**Community Impact**: Use of inappropriate language or other behavior deemed
unprofessional or unwelcome in the community.
**Consequence**: A private, written warning from community leaders, providing
clarity around the nature of the violation and an explanation of why the
behavior was inappropriate. A public apology may be requested.
### 2. Warning
**Community Impact**: A violation through a single incident or series
of actions.
**Consequence**: A warning with consequences for continued behavior. No
interaction with the people involved, including unsolicited interaction with
those enforcing the Code of Conduct, for a specified period of time. This
includes avoiding interactions in community spaces as well as external channels
like social media. Violating these terms may lead to a temporary or
permanent ban.
### 3. Temporary Ban
**Community Impact**: A serious violation of community standards, including
sustained inappropriate behavior.
**Consequence**: A temporary ban from any sort of interaction or public
communication with the community for a specified period of time. No public or
private interaction with the people involved, including unsolicited interaction
with those enforcing the Code of Conduct, is allowed during this period.
Violating these terms may lead to a permanent ban.
### 4. Permanent Ban
**Community Impact**: Demonstrating a pattern of violation of community
standards, including sustained inappropriate behavior, harassment of an
individual, or aggression toward or disparagement of classes of individuals.
**Consequence**: A permanent ban from any sort of public interaction within
the community.
## Attribution
This Code of Conduct is adapted from the [Contributor Covenant][homepage],
version 2.0, available at
https://www.contributor-covenant.org/version/2/0/code_of_conduct.html.
Community Impact Guidelines were inspired by [Mozilla's code of conduct
enforcement ladder](https://github.com/mozilla/diversity).
[homepage]: https://www.contributor-covenant.org
For answers to common questions about this code of conduct, see the FAQ at
https://www.contributor-covenant.org/faq. Translations are available at
https://www.contributor-covenant.org/translations.
+130
View File
@@ -0,0 +1,130 @@
<!-- omit in toc -->
# Contributing to gainmap-js
First off, thanks for taking the time to contribute! ❤️
All types of contributions are encouraged and valued. See the [Table of Contents](#table-of-contents) for different ways to help and details about how this project handles them. Please make sure to read the relevant section before making your contribution. It will make it a lot easier for us maintainers and smooth out the experience for all involved. The community looks forward to your contributions. 🎉
> And if you like the project, but just don't have time to contribute, that's fine. There are other easy ways to support the project and show your appreciation, which we would also be very happy about:
> - Star the project
> - Tweet about it
> - Refer this project in your project's readme
> - Mention the project at local meetups and tell your friends/colleagues
<!-- omit in toc -->
## Table of Contents
- [I Have a Question](#i-have-a-question)
- [I Want To Contribute](#i-want-to-contribute)
- [Reporting Bugs](#reporting-bugs)
- [Suggesting Enhancements](#suggesting-enhancements)
- [Your First Code Contribution](#your-first-code-contribution)
- [Improving The Documentation](#improving-the-documentation)
- [Styleguides](#styleguides)
- [Commit Messages](#commit-messages)
- [Join The Project Team](#join-the-project-team)
## I Have a Question
> If you want to ask a question, we assume that you have read the available [Documentation](https://github.com/MONOGRID/gainmap-js/wiki).
Before you ask a question, it is best to search for existing [Issues](https://github.com/MONOGRID/gainmap-js/issues) that might help you. In case you have found a suitable issue and still need clarification, you can write your question in this issue. It is also advisable to search the internet for answers first.
If you then still feel the need to ask a question and need clarification, we recommend the following:
- Open an [Issue](https://github.com/MONOGRID/gainmap-js/issues/new).
- Provide as much context as you can about what you're running into.
- Provide project and platform versions (nodejs, npm, etc), depending on what seems relevant.
We will then take care of the issue as soon as possible.
<!--
You might want to create a separate issue tag for questions and include it in this description. People should then tag their issues accordingly.
Depending on how large the project is, you may want to outsource the questioning, e.g. to Stack Overflow or Gitter. You may add additional contact and information possibilities:
- IRC
- Slack
- Gitter
- Stack Overflow tag
- Blog
- FAQ
- Roadmap
- E-Mail List
- Forum
-->
## I Want To Contribute
> ### Legal Notice <!-- omit in toc -->
> When contributing to this project, you must agree that you have authored 100% of the content, that you have the necessary rights to the content and that the content you contribute may be provided under the project license.
### Reporting Bugs
<!-- omit in toc -->
#### Before Submitting a Bug Report
A good bug report shouldn't leave others needing to chase you up for more information. Therefore, we ask you to investigate carefully, collect information and describe the issue in detail in your report. Please complete the following steps in advance to help us fix any potential bug as fast as possible.
- Make sure that you are using the latest version.
- Determine if your bug is really a bug and not an error on your side e.g. using incompatible environment components/versions (Make sure that you have read the [documentation](https://github.com/MONOGRID/gainmap-js/wiki). If you are looking for support, you might want to check [this section](#i-have-a-question)).
- To see if other users have experienced (and potentially already solved) the same issue you are having, check if there is not already a bug report existing for your bug or error in the [bug tracker](https://github.com/MONOGRID/gainmap-jsissues?q=label%3Abug).
- Also make sure to search the internet (including Stack Overflow) to see if users outside of the GitHub community have discussed the issue.
- Collect information about the bug:
- Stack trace (Traceback)
- OS, Platform and Version (Windows, Linux, macOS, x86, ARM)
- Version of the interpreter, compiler, SDK, runtime environment, package manager, depending on what seems relevant.
- Possibly your input and the output
- Can you reliably reproduce the issue? And can you also reproduce it with older versions?
<!-- omit in toc -->
#### How Do I Submit a Good Bug Report?
> You must never report security related issues, vulnerabilities or bugs including sensitive information to the issue tracker, or elsewhere in public. Instead sensitive bugs must be sent by email to <gainmap@monogrid.com>.
<!-- You may add a PGP key to allow the messages to be sent encrypted as well. -->
We use GitHub issues to track bugs and errors. If you run into an issue with the project:
- Open an [Issue](https://github.com/MONOGRID/gainmap-js/issues/new). (Since we can't be sure at this point whether it is a bug or not, we ask you not to talk about a bug yet and not to label the issue.)
- Explain the behavior you would expect and the actual behavior.
- Please provide as much context as possible and describe the *reproduction steps* that someone else can follow to recreate the issue on their own. This usually includes your code. For good bug reports you should isolate the problem and create a reduced test case.
- Provide the information you collected in the previous section.
Once it's filed:
- The project team will label the issue accordingly.
- A team member will try to reproduce the issue with your provided steps. If there are no reproduction steps or no obvious way to reproduce the issue, the team will ask you for those steps and mark the issue as `needs-repro`. Bugs with the `needs-repro` tag will not be addressed until they are reproduced.
- If the team is able to reproduce the issue, it will be marked `needs-fix`, as well as possibly other tags (such as `critical`), and the issue will be left to be [implemented by someone](#your-first-code-contribution).
<!-- You might want to create an issue template for bugs and errors that can be used as a guide and that defines the structure of the information to be included. If you do so, reference it here in the description. -->
### Suggesting Enhancements
This section guides you through submitting an enhancement suggestion for gainmap-js, **including completely new features and minor improvements to existing functionality**. Following these guidelines will help maintainers and the community to understand your suggestion and find related suggestions.
<!-- omit in toc -->
#### Before Submitting an Enhancement
- Make sure that you are using the latest version.
- Read the [documentation](https://github.com/MONOGRID/gainmap-js/wiki) carefully and find out if the functionality is already covered, maybe by an individual configuration.
- Perform a [search](https://github.com/MONOGRID/gainmap-js/issues) to see if the enhancement has already been suggested. If it has, add a comment to the existing issue instead of opening a new one.
- Find out whether your idea fits with the scope and aims of the project. It's up to you to make a strong case to convince the project's developers of the merits of this feature. Keep in mind that we want features that will be useful to the majority of our users and not just a small subset. If you're just targeting a minority of users, consider writing an add-on/plugin library.
<!-- omit in toc -->
#### How Do I Submit a Good Enhancement Suggestion?
Enhancement suggestions are tracked as [GitHub issues](https://github.com/MONOGRID/gainmap-js/issues).
- Use a **clear and descriptive title** for the issue to identify the suggestion.
- Provide a **step-by-step description of the suggested enhancement** in as many details as possible.
- **Describe the current behavior** and **explain which behavior you expected to see instead** and why. At this point you can also tell which alternatives do not work for you.
- You may want to **include screenshots and animated GIFs** which help you demonstrate the steps or point out the part which the suggestion is related to. You can use [this tool](https://www.cockos.com/licecap/) to record GIFs on macOS and Windows, and [this tool](https://github.com/colinkeenan/silentcast) or [this tool](https://github.com/GNOME/byzanz) on Linux. <!-- this should only be included if the project has a GUI -->
- **Explain why this enhancement would be useful** to most gainmap-js users. You may also want to point out the other projects that solved it better and which could serve as inspiration.
<!-- omit in toc -->
## Attribution
This guide is based on the **contributing-gen**. [Make your own](https://github.com/bttger/contributing-gen)!
+21
View File
@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2023 MONOGRID
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
+371
View File
@@ -0,0 +1,371 @@
# gainmap-js
[![FOSSA Status](https://app.fossa.com/api/projects/git%2Bgithub.com%2FMONOGRID%2Fgainmap-js.svg?type=shield)](https://app.fossa.com/projects/git%2Bgithub.com%2FMONOGRID%2Fgainmap-js?ref=badge_shield)
A Javascript (TypeScript) Encoder/Decoder Implementation of Adobe's Gain Map Technology for storing HDR Images using an SDR Image + a "Gain map"
> :warning: This library is intended for encoding and decoding gain map images for the [three.js](https://github.com/mrdoob/three.js/) 3D Library
>
> It can be used for general encode/decode of gain maps but it depends on the three.js library which, in itself, is quite heavy if you only use it to encode/decode gain maps.
## Share Your Project!
Have you built something awesome using this library? Wed love to see what youve created!
Whether its a small demo or a full-scale application, your project can inspire others and help improve the library. Please share your work in one of these ways:
* GitHub Issues: [Open a new issue](https://github.com/MONOGRID/gainmap-js/issues/new) in this repository with a brief description, screenshots, and links to your project.
* Email: Send us an email at gainmap@monogrid.com with your project details.
Your contributions and creative ideas make our community stronger. We cant wait to see what you build!
## Live Demo
* [WebGPU Version](https://monogrid.github.io/gainmap-js/webgpu.html)
* [Legacy WebGL Version](https://monogrid.github.io/gainmap-js/)
Both compares loading:
1. a `JPEG` file with embedded gain map data
2. a `webp` sdr file + a `webp` gain map + metadata JSON
3. a comparable size `.hdr` file for comparison
## [Free online encoder](https://gainmap-creator.monogrid.com)
Use it to convert `.hdr` and `.exr` files into gain maps. It's free and the whole process happens in your browser.
## Installing
```bash
$ npm install @monogrid/gainmap-js three
```
## What is a Gain map?
[See here](https://gregbenzphotography.com/hdr-images/jpg-hdr-gain-maps-in-adobe-camera-raw/) for a detailed explanation, here are some relevant parts:
> A gain map is a single file with a second pseudo-image embedded in it to create an optimized result for a specific monitor. It can be used to generate the HDR version (which looks dramatically better where supported), the SDR version (without tone mapping to ensures great quality), or anything in between (to better support less capable HDR displays).
> Gain maps are not a new type of file, but rather a technology which can be embedded into a variety of image formats. There are reference specs already for the JPG, AVIF, JXL, and HEIF file formats. JPG is especially notable as it could not properly support HDR without gain maps and it offers a very useful bridge to the future (i.e. highly compatible with todays software).
> A gain map includes:
>
> * A **base (default) image**. This can be an SDR or an HDR image (JPG gain maps are always encoded with SDR as the base). If the browser or viewing software does not understand gain maps, it will just the treat file as if it were just the base image.
> * The **gain map**. This is a secondary “image” embedded in the file. It is not a real image, but rather contains data to convert each pixel from the base image into the other (SDR or HDR) version of the image.
>* Gain map **metadata**. This tells the browser how the gain map is encoded as well as critical information to optimize rendering on any display.
Please note that Google [is adopting the gain map technology in Android 14](https://support.google.com/photos/answer/14159275) but its naming of the technology refers to it as **Ultra HDR Image Format** and a JPEG file with embedded gain map is [apparently called JPEGR](https://github.com/google/libultrahdr/blob/3a3a752a5da0b2304b1b6de0ef383bbe41256a67/lib/jpegr.h#L28C16-L28C21) in their terminology, we call it `HDRJPEG` for the moment.
## API
Refer to the [WIKI](https://github.com/MONOGRID/gainmap-js/wiki) for detailed documentation about the API.
## Examples
### Decoding
The main use case of this library is to decode a JPEG file that contains gain map data
and use it instead of a traditional `.exr` or `.hdr` image.
> NOTE: Starting from v3.1.0 Decoding also works in a WebWorker, thanks to [Alejandro Romano](https://github.com/Arecsu) for the [contribution](https://github.com/MONOGRID/gainmap-js/pull/59)
### Using a single JPEG with embedded Gain map Metadata
This approach lets you load a single file with an embedded Gain Map.
The advantage is to have a single file to load.
The disadvantages are:
* No WEBP compression
* The JPEG cannot be manipulated in Photoshop, GIMP, or any other software that does not support the gain map technology (no photo editing software supports it at the time of writing 06-11-2023).
* Photo sharing websites and/or services (i.e. sharing with Slack) will likely strip the Gain map metadata and the HDR information will be lost, leaving you with only the SDR Representation.
```ts
import { HDRJPGLoader } from '@monogrid/gainmap-js'
import {
EquirectangularReflectionMapping,
Mesh,
MeshBasicMaterial,
PerspectiveCamera,
PlaneGeometry,
Scene,
WebGLRenderer
} from 'three'
const renderer = new WebGLRenderer()
const loader = new HDRJPGLoader(renderer)
.setRenderTargetOptions({ mapping: EquirectangularReflectionMapping })
const result = await loader.loadAsync('gainmap.jpeg')
// `result` can be used to populate a Texture
const scene = new Scene()
const mesh = new Mesh(
new PlaneGeometry(),
new MeshBasicMaterial({ map: result.renderTarget.texture })
)
scene.add(mesh)
renderer.render(scene, new PerspectiveCamera())
// Starting from three.js r159
// `result.renderTarget.texture` can
// also be used as Equirectangular scene background
//
// it was previously needed to convert it
// to a DataTexture with `result.toDataTexture()`
scene.background = result.renderTarget.texture
// result must be manually disposed
// when you are done using it
result.dispose()
```
### Using separate files
Using separate files will get rid of the limitations of using a single JPEG file but it will force to use three separate files
1. An SDR Representation file
2. A Gainmap file
3. A JSON containing the gainmap metadata used for decoding
```ts
import { GainMapLoader } from '@monogrid/gainmap-js'
import {
EquirectangularReflectionMapping,
Mesh,
MeshBasicMaterial,
PerspectiveCamera,
PlaneGeometry,
Scene,
WebGLRenderer
} from 'three'
const renderer = new WebGLRenderer()
const loader = new GainMapLoader(renderer)
.setRenderTargetOptions({ mapping: EquirectangularReflectionMapping })
const result = await loader.loadAsync(['sdr.jpeg', 'gainmap.jpeg', 'metadata.json'])
// `result` can be used to populate a Texture
const scene = new Scene()
const mesh = new Mesh(
new PlaneGeometry(),
new MeshBasicMaterial({ map: result.renderTarget.texture })
)
scene.add(mesh)
renderer.render(scene, new PerspectiveCamera())
// Starting from three.js r159
// `result.renderTarget.texture` can
// also be used as Equirectangular scene background
//
// it was previously needed to convert it
// to a DataTexture with `result.toDataTexture()`
scene.background = result.renderTarget.texture
// result must be manually disposed
// when you are done using it
result.dispose()
```
### Decoding with WebGPU
> NOTE: WebGPU decoding requires three.js r163 or higher and a browser that supports WebGPU
The library provides WebGPU-accelerated decoding through the `@monogrid/gainmap-js/webgpu` entry point. WebGPU decoding uses Three.js Shading Language (TSL) and offers improved performance and modern GPU architecture support.
#### Using a single JPEG with embedded Gain map Metadata (WebGPU)
```ts
import { HDRJPGLoader } from '@monogrid/gainmap-js/webgpu'
import {
EquirectangularReflectionMapping,
Mesh,
MeshBasicMaterial,
PerspectiveCamera,
PlaneGeometry,
Scene,
WebGPURenderer
} from 'three/webgpu'
const renderer = new WebGPURenderer()
await renderer.init()
const loader = new HDRJPGLoader(renderer)
.setRenderTargetOptions({ mapping: EquirectangularReflectionMapping })
const result = await loader.loadAsync('gainmap.jpeg')
// `result` can be used to populate a Texture
const scene = new Scene()
const mesh = new Mesh(
new PlaneGeometry(),
new MeshBasicMaterial({ map: result.renderTarget.texture })
)
scene.add(mesh)
renderer.render(scene, new PerspectiveCamera())
// Starting from three.js r159
// `result.renderTarget.texture` can
// also be used as Equirectangular scene background
scene.background = result.renderTarget.texture
// result must be manually disposed
// when you are done using it
result.dispose()
```
#### Using separate files (WebGPU)
```ts
import { GainMapLoader } from '@monogrid/gainmap-js/webgpu'
import {
EquirectangularReflectionMapping,
Mesh,
MeshBasicMaterial,
PerspectiveCamera,
PlaneGeometry,
Scene,
WebGPURenderer
} from 'three/webgpu'
const renderer = new WebGPURenderer()
await renderer.init()
const loader = new GainMapLoader(renderer)
.setRenderTargetOptions({ mapping: EquirectangularReflectionMapping })
const result = await loader.loadAsync(['sdr.jpeg', 'gainmap.jpeg', 'metadata.json'])
// `result` can be used to populate a Texture
const scene = new Scene()
const mesh = new Mesh(
new PlaneGeometry(),
new MeshBasicMaterial({ map: result.renderTarget.texture })
)
scene.add(mesh)
renderer.render(scene, new PerspectiveCamera())
// Starting from three.js r159
// `result.renderTarget.texture` can
// also be used as Equirectangular scene background
scene.background = result.renderTarget.texture
// result must be manually disposed
// when you are done using it
result.dispose()
```
#### Key differences between WebGL and WebGPU
1. **Import path**: Use `@monogrid/gainmap-js/webgpu` instead of `@monogrid/gainmap-js`
2. **Three.js imports**: Use `three/webgpu` instead of `three`
3. **Renderer**: Use `WebGPURenderer` instead of `WebGLRenderer`
4. **Renderer initialization**: Call `await renderer.init()` before using WebGPU renderer
5. **Async decoding**: The `decode()` function is async in WebGPU
### Encoding
Encoding a Gain map starting from an EXR file.
This is generally not useful in a `three.js` site but this library exposes methods
that allow to encode an `.exr` or `hdr` file into a `jpeg` with an embedded gain map.
```ts
import { compress, encode, findTextureMinMax } from '@monogrid/gainmap-js/encode'
import { encodeJPEGMetadata } from '@monogrid/gainmap-js/libultrahdr'
import { EXRLoader } from 'three/examples/jsm/loaders/EXRLoader.js'
// load an HDR file
const loader = new EXRLoader()
const image = await loader.loadAsync('image.exr')
// find RAW RGB Max value of a texture
const textureMax = findTextureMinMax(image)
// Encode the gainmap
const encodingResult = encode({
image,
// this will encode the full HDR range
maxContentBoost: Math.max.apply(this, textureMax)
})
// obtain the RAW RGBA SDR buffer and create an ImageData
const sdrImageData = new ImageData(encodingResult.sdr.toArray(), encodingResult.sdr.width, encodingResult.sdr.height)
// obtain the RAW RGBA Gain map buffer and create an ImageData
const gainMapImageData = new ImageData(encodingResult.gainMap.toArray(), encodingResult.gainMap.width, encodingResult.gainMap.height)
// parallel compress the RAW buffers into the specified mimeType
const mimeType = 'image/jpeg'
const quality = 0.9
const [sdr, gainMap] = await Promise.all([
compress({
source: sdrImageData,
mimeType,
quality,
flipY: true // output needs to be flipped
}),
compress({
source: gainMapImageData,
mimeType,
quality,
flipY: true // output needs to be flipped
})
])
// obtain the metadata which will be embedded into
// and XMP tag inside the final JPEG file
const metadata = encodingResult.getMetadata()
// embed the compressed images + metadata into a single
// JPEG file
const jpeg = encodeJPEGMetadata({
...encodingResult,
...metadata,
sdr,
gainMap
})
// `jpeg` will be an `Uint8Array` which can be saved somewhere
// encoder must be manually disposed
// when no longer needed
encodingResult.gainMap.dispose()
encodingResult.sdr.dispose()
```
## Libultrahdr in Vite
If you import `@monogrid/gainmap-js/libultrahdr`
You will need to exclude it from Vite optimizations.
```js
// vite.config.js
module.exports = defineConfig({
...
optimizeDeps: {
exclude: ['@monogrid/gainmap-js/libultrahdr']
},
...
})
```
## Building
Clone the repository:
```bash
$ git clone git@github.com:MONOGRID/gainmap-js.git
$ cd gainmap-js
$ npm i
$ npm run build
```
## References
* [Adobe Gainmap Specification](https://helpx.adobe.com/camera-raw/using/gain-map.html)
* [Ultra HDR Image Format v1.0](https://developer.android.com/guide/topics/media/platform/hdr-image-format)
## License
[![FOSSA Status](https://app.fossa.com/api/projects/git%2Bgithub.com%2FMONOGRID%2Fgainmap-js.svg?type=large)](https://app.fossa.com/projects/git%2Bgithub.com%2FMONOGRID%2Fgainmap-js?ref=badge_large)
File diff suppressed because one or more lines are too long
+424
View File
@@ -0,0 +1,424 @@
/**
* @monogrid/gainmap-js v3.4.0
* With ❤️, by MONOGRID <gainmap@monogrid.com>
*/
import { SRGBColorSpace, LinearSRGBColorSpace, HalfFloatType, Loader, LoadingManager, Texture, UVMapping, ClampToEdgeWrapping, LinearFilter, LinearMipMapLinearFilter, RGBAFormat, UnsignedByteType } from 'three';
/**
* Shared decode implementation factory
* Creates a decode function that prepares a QuadRenderer with the given parameters
*/
function createDecodeFunction(config) {
return (params) => {
const { sdr, gainMap, renderer } = params;
if (sdr.colorSpace !== SRGBColorSpace) {
console.warn('SDR Colorspace needs to be *SRGBColorSpace*, setting it automatically');
sdr.colorSpace = SRGBColorSpace;
}
sdr.needsUpdate = true;
if (gainMap.colorSpace !== LinearSRGBColorSpace) {
console.warn('Gainmap Colorspace needs to be *LinearSRGBColorSpace*, setting it automatically');
gainMap.colorSpace = LinearSRGBColorSpace;
}
gainMap.needsUpdate = true;
const material = config.createMaterial({
...params,
sdr,
gainMap
});
const quadRenderer = config.createQuadRenderer({
width: sdr.image.width,
height: sdr.image.height,
type: HalfFloatType,
colorSpace: LinearSRGBColorSpace,
material,
renderer,
renderTargetOptions: params.renderTargetOptions
});
return quadRenderer;
};
}
class GainMapNotFoundError extends Error {
}
class XMPMetadataNotFoundError extends Error {
}
const getXMLValue = (xml, tag, defaultValue) => {
// Check for attribute format first: tag="value"
const attributeMatch = new RegExp(`${tag}="([^"]*)"`, 'i').exec(xml);
if (attributeMatch)
return attributeMatch[1];
// Check for tag format: <tag>value</tag> or <tag><rdf:li>value</rdf:li>...</tag>
const tagMatch = new RegExp(`<${tag}[^>]*>([\\s\\S]*?)</${tag}>`, 'i').exec(xml);
if (tagMatch) {
// Check if it contains rdf:li elements
const liValues = tagMatch[1].match(/<rdf:li>([^<]*)<\/rdf:li>/g);
if (liValues && liValues.length === 3) {
return liValues.map(v => v.replace(/<\/?rdf:li>/g, ''));
}
return tagMatch[1].trim();
}
if (defaultValue !== undefined)
return defaultValue;
throw new Error(`Can't find ${tag} in gainmap metadata`);
};
const extractXMP = (input) => {
let str;
// support node test environment
if (typeof TextDecoder !== 'undefined')
str = new TextDecoder().decode(input);
else
str = input.toString();
let start = str.indexOf('<x:xmpmeta');
while (start !== -1) {
const end = str.indexOf('x:xmpmeta>', start);
const xmpBlock = str.slice(start, end + 10);
try {
const gainMapMin = getXMLValue(xmpBlock, 'hdrgm:GainMapMin', '0');
const gainMapMax = getXMLValue(xmpBlock, 'hdrgm:GainMapMax');
const gamma = getXMLValue(xmpBlock, 'hdrgm:Gamma', '1');
const offsetSDR = getXMLValue(xmpBlock, 'hdrgm:OffsetSDR', '0.015625');
const offsetHDR = getXMLValue(xmpBlock, 'hdrgm:OffsetHDR', '0.015625');
// These are always attributes, so we can use a simpler regex
const hdrCapacityMinMatch = /hdrgm:HDRCapacityMin="([^"]*)"/.exec(xmpBlock);
const hdrCapacityMin = hdrCapacityMinMatch ? hdrCapacityMinMatch[1] : '0';
const hdrCapacityMaxMatch = /hdrgm:HDRCapacityMax="([^"]*)"/.exec(xmpBlock);
if (!hdrCapacityMaxMatch)
throw new Error('Incomplete gainmap metadata');
const hdrCapacityMax = hdrCapacityMaxMatch[1];
return {
gainMapMin: Array.isArray(gainMapMin) ? gainMapMin.map(v => parseFloat(v)) : [parseFloat(gainMapMin), parseFloat(gainMapMin), parseFloat(gainMapMin)],
gainMapMax: Array.isArray(gainMapMax) ? gainMapMax.map(v => parseFloat(v)) : [parseFloat(gainMapMax), parseFloat(gainMapMax), parseFloat(gainMapMax)],
gamma: Array.isArray(gamma) ? gamma.map(v => parseFloat(v)) : [parseFloat(gamma), parseFloat(gamma), parseFloat(gamma)],
offsetSdr: Array.isArray(offsetSDR) ? offsetSDR.map(v => parseFloat(v)) : [parseFloat(offsetSDR), parseFloat(offsetSDR), parseFloat(offsetSDR)],
offsetHdr: Array.isArray(offsetHDR) ? offsetHDR.map(v => parseFloat(v)) : [parseFloat(offsetHDR), parseFloat(offsetHDR), parseFloat(offsetHDR)],
hdrCapacityMin: parseFloat(hdrCapacityMin),
hdrCapacityMax: parseFloat(hdrCapacityMax)
};
}
catch (e) {
// Continue searching for another xmpmeta block if this one fails
}
start = str.indexOf('<x:xmpmeta', end);
}
};
/**
* MPF Extractor (Multi Picture Format Extractor)
* By Henrik S Nilsson 2019
*
* Extracts images stored in images based on the MPF format (found here: https://www.cipa.jp/e/std/std-sec.html
* under "CIPA DC-007-Translation-2021 Multi-Picture Format"
*
* Overly commented, and without intention of being complete or production ready.
* Created to extract depth maps from iPhone images, and to learn about image metadata.
* Kudos to: Phil Harvey (exiftool), Jaume Sanchez (android-lens-blur-depth-extractor)
*/
class MPFExtractor {
options;
constructor(options) {
this.options = {
debug: options && options.debug !== undefined ? options.debug : false,
extractFII: options && options.extractFII !== undefined ? options.extractFII : true,
extractNonFII: options && options.extractNonFII !== undefined ? options.extractNonFII : true
};
}
extract(imageArrayBuffer) {
return new Promise((resolve, reject) => {
const debug = this.options.debug;
const dataView = new DataView(imageArrayBuffer.buffer);
// If you're executing this line on a big endian machine, it'll be reversed.
// bigEnd further down though, refers to the endianness of the image itself.
if (dataView.getUint16(0) !== 0xffd8) {
reject(new Error('Not a valid jpeg'));
return;
}
const length = dataView.byteLength;
let offset = 2;
let loops = 0;
let marker; // APP# marker
while (offset < length) {
if (++loops > 250) {
reject(new Error(`Found no marker after ${loops} loops 😵`));
return;
}
if (dataView.getUint8(offset) !== 0xff) {
reject(new Error(`Not a valid marker at offset 0x${offset.toString(16)}, found: 0x${dataView.getUint8(offset).toString(16)}`));
return;
}
marker = dataView.getUint8(offset + 1);
if (debug)
console.log(`Marker: ${marker.toString(16)}`);
if (marker === 0xe2) {
if (debug)
console.log('Found APP2 marker (0xffe2)');
// Works for iPhone 8 Plus, X, and XSMax. Or any photos of MPF format.
// Great way to visualize image information in html is using Exiftool. E.g.:
// ./exiftool.exe -htmldump -wantTrailer photo.jpg > photo.html
const formatPt = offset + 4;
/*
* Structure of the MP Format Identifier
*
* Offset Addr. | Code (Hex) | Description
* +00 ff Marker Prefix <-- offset
* +01 e2 APP2
* +02 #n APP2 Field Length
* +03 #n APP2 Field Length
* +04 4d 'M' <-- formatPt
* +05 50 'P'
* +06 46 'F'
* +07 00 NULL
* <-- tiffOffset
*/
if (dataView.getUint32(formatPt) === 0x4d504600) {
// Found MPF tag, so we start dig out sub images
const tiffOffset = formatPt + 4;
let bigEnd; // Endianness from TIFF header
// Test for TIFF validity and endianness
// 0x4949 and 0x4D4D ('II' and 'MM') marks Little Endian and Big Endian
if (dataView.getUint16(tiffOffset) === 0x4949) {
bigEnd = false;
}
else if (dataView.getUint16(tiffOffset) === 0x4d4d) {
bigEnd = true;
}
else {
reject(new Error('No valid endianness marker found in TIFF header'));
return;
}
if (dataView.getUint16(tiffOffset + 2, !bigEnd) !== 0x002a) {
reject(new Error('Not valid TIFF data! (no 0x002A marker)'));
return;
}
// 32 bit number stating the offset from the start of the 8 Byte MP Header
// to MP Index IFD Least possible value is thus 8 (means 0 offset)
const firstIFDOffset = dataView.getUint32(tiffOffset + 4, !bigEnd);
if (firstIFDOffset < 0x00000008) {
reject(new Error('Not valid TIFF data! (First offset less than 8)'));
return;
}
// Move ahead to MP Index IFD
// Assume we're at the first IFD, so firstIFDOffset points to
// MP Index IFD and not MP Attributes IFD. (If we try extract from a sub image,
// we fail silently here due to this assumption)
// Count (2 Byte) | MP Index Fields a.k.a. MP Entries (count * 12 Byte) | Offset of Next IFD (4 Byte)
const dirStart = tiffOffset + firstIFDOffset; // Start of IFD (Image File Directory)
const count = dataView.getUint16(dirStart, !bigEnd); // Count of MPEntries (2 Byte)
// Extract info from MPEntries (starting after Count)
const entriesStart = dirStart + 2;
let numberOfImages = 0;
for (let i = entriesStart; i < entriesStart + 12 * count; i += 12) {
// Each entry is 12 Bytes long
// Check MP Index IFD tags, here we only take tag 0xb001 = Number of images
if (dataView.getUint16(i, !bigEnd) === 0xb001) {
// stored in Last 4 bytes of its 12 Byte entry.
numberOfImages = dataView.getUint32(i + 8, !bigEnd);
}
}
const nextIFDOffsetLen = 4; // 4 Byte offset field that appears after MP Index IFD tags
const MPImageListValPt = dirStart + 2 + count * 12 + nextIFDOffsetLen;
const images = [];
for (let i = MPImageListValPt; i < MPImageListValPt + numberOfImages * 16; i += 16) {
const image = {
MPType: dataView.getUint32(i, !bigEnd),
size: dataView.getUint32(i + 4, !bigEnd),
// This offset is specified relative to the address of the MP Endian
// field in the MP Header, unless the image is a First Individual Image,
// in which case the value of the offset shall be NULL (0x00000000).
dataOffset: dataView.getUint32(i + 8, !bigEnd),
dependantImages: dataView.getUint32(i + 12, !bigEnd),
start: -1,
end: -1,
isFII: false
};
if (!image.dataOffset) {
// dataOffset is 0x00000000 for First Individual Image
image.start = 0;
image.isFII = true;
}
else {
image.start = tiffOffset + image.dataOffset;
image.isFII = false;
}
image.end = image.start + image.size;
images.push(image);
}
if (this.options.extractNonFII && images.length) {
const bufferBlob = new Blob([dataView]);
const imgs = [];
for (const image of images) {
if (image.isFII && !this.options.extractFII) {
continue; // Skip FII
}
const imageBlob = bufferBlob.slice(image.start, image.end + 1, 'image/jpeg');
// we don't need this
// const imageUrl = URL.createObjectURL(imageBlob)
// image.img = document.createElement('img')
// image.img.src = imageUrl
imgs.push(imageBlob);
}
resolve(imgs);
}
}
}
offset += 2 + dataView.getUint16(offset + 2);
}
});
}
}
/**
* Extracts XMP Metadata and the gain map recovery image
* from a single JPEG file.
*
* @category Decoding Functions
* @group Decoding Functions
* @param jpegFile an `Uint8Array` containing and encoded JPEG file
* @returns an sdr `Uint8Array` compressed in JPEG, a gainMap `Uint8Array` compressed in JPEG and the XMP parsed XMP metadata
* @throws Error if XMP Metadata is not found
* @throws Error if Gain map image is not found
* @example
* import { FileLoader } from 'three'
* import { extractGainmapFromJPEG } from '@monogrid/gainmap-js'
*
* const jpegFile = await new FileLoader()
* .setResponseType('arraybuffer')
* .loadAsync('image.jpg')
*
* const { sdr, gainMap, metadata } = extractGainmapFromJPEG(jpegFile)
*/
const extractGainmapFromJPEG = async (jpegFile) => {
const metadata = extractXMP(jpegFile);
if (!metadata)
throw new XMPMetadataNotFoundError('Gain map XMP metadata not found');
const mpfExtractor = new MPFExtractor({ extractFII: true, extractNonFII: true });
const images = await mpfExtractor.extract(jpegFile);
if (images.length !== 2)
throw new GainMapNotFoundError('Gain map recovery image not found');
return {
sdr: new Uint8Array(await images[0].arrayBuffer()),
gainMap: new Uint8Array(await images[1].arrayBuffer()),
metadata
};
};
/**
* private function, async get image from blob
*
* @param blob
* @returns
*/
const getHTMLImageFromBlob = (blob) => {
return new Promise((resolve, reject) => {
const img = document.createElement('img');
img.onload = () => { resolve(img); };
// eslint-disable-next-line @typescript-eslint/prefer-promise-reject-errors
img.onerror = (e) => { reject(e); };
img.src = URL.createObjectURL(blob);
});
};
/**
* Shared base class for loaders that extracts common logic
*/
class LoaderBaseShared extends Loader {
_renderer;
_renderTargetOptions;
_internalLoadingManager;
_config;
constructor(config, manager) {
super(manager);
this._config = config;
if (config.renderer)
this._renderer = config.renderer;
this._internalLoadingManager = new LoadingManager();
}
setRenderer(renderer) {
this._renderer = renderer;
return this;
}
setRenderTargetOptions(options) {
this._renderTargetOptions = options;
return this;
}
prepareQuadRenderer() {
if (!this._renderer) {
console.warn('WARNING: A 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.');
}
const material = this._config.createMaterial({
gainMapMax: [1, 1, 1],
gainMapMin: [0, 0, 0],
gamma: [1, 1, 1],
offsetHdr: [1, 1, 1],
offsetSdr: [1, 1, 1],
hdrCapacityMax: 1,
hdrCapacityMin: 0,
maxDisplayBoost: 1,
gainMap: new Texture(),
sdr: new Texture()
});
return this._config.createQuadRenderer({
width: 16,
height: 16,
type: HalfFloatType,
colorSpace: LinearSRGBColorSpace,
material,
renderer: this._renderer,
renderTargetOptions: this._renderTargetOptions
});
}
async processImages(sdrBuffer, gainMapBuffer, imageOrientation) {
const gainMapBlob = gainMapBuffer ? new Blob([gainMapBuffer], { type: 'image/jpeg' }) : undefined;
const sdrBlob = new Blob([sdrBuffer], { type: 'image/jpeg' });
let sdrImage;
let gainMapImage;
let needsFlip = false;
if (typeof createImageBitmap === 'undefined') {
const res = await Promise.all([
gainMapBlob ? getHTMLImageFromBlob(gainMapBlob) : Promise.resolve(undefined),
getHTMLImageFromBlob(sdrBlob)
]);
gainMapImage = res[0];
sdrImage = res[1];
needsFlip = imageOrientation === 'flipY';
}
else {
const res = await Promise.all([
gainMapBlob ? createImageBitmap(gainMapBlob, { imageOrientation: imageOrientation || 'flipY' }) : Promise.resolve(undefined),
createImageBitmap(sdrBlob, { imageOrientation: imageOrientation || 'flipY' })
]);
gainMapImage = res[0];
sdrImage = res[1];
}
return { sdrImage, gainMapImage, needsFlip };
}
createTextures(sdrImage, gainMapImage, needsFlip) {
const gainMap = new Texture(gainMapImage || new ImageData(2, 2), UVMapping, ClampToEdgeWrapping, ClampToEdgeWrapping, LinearFilter, LinearMipMapLinearFilter, RGBAFormat, UnsignedByteType, 1, LinearSRGBColorSpace);
gainMap.flipY = needsFlip;
gainMap.needsUpdate = true;
const sdr = new Texture(sdrImage, UVMapping, ClampToEdgeWrapping, ClampToEdgeWrapping, LinearFilter, LinearMipMapLinearFilter, RGBAFormat, UnsignedByteType, 1, SRGBColorSpace);
sdr.flipY = needsFlip;
sdr.needsUpdate = true;
return { gainMap, sdr };
}
updateQuadRenderer(quadRenderer, sdrImage, gainMap, sdr, metadata) {
quadRenderer.width = sdrImage.width;
quadRenderer.height = sdrImage.height;
quadRenderer.material.gainMap = gainMap;
quadRenderer.material.sdr = sdr;
quadRenderer.material.gainMapMin = metadata.gainMapMin;
quadRenderer.material.gainMapMax = metadata.gainMapMax;
quadRenderer.material.offsetHdr = metadata.offsetHdr;
quadRenderer.material.offsetSdr = metadata.offsetSdr;
quadRenderer.material.gamma = metadata.gamma;
quadRenderer.material.hdrCapacityMin = metadata.hdrCapacityMin;
quadRenderer.material.hdrCapacityMax = metadata.hdrCapacityMax;
quadRenderer.material.maxDisplayBoost = Math.pow(2, metadata.hdrCapacityMax);
quadRenderer.material.needsUpdate = true;
}
}
export { GainMapNotFoundError as G, LoaderBaseShared as L, MPFExtractor as M, XMPMetadataNotFoundError as X, extractXMP as a, createDecodeFunction as c, extractGainmapFromJPEG as e };
+310
View File
@@ -0,0 +1,310 @@
/**
* @monogrid/gainmap-js v3.4.0
* With ❤️, by MONOGRID <gainmap@monogrid.com>
*/
import { ClampToEdgeWrapping, LinearFilter, Scene, OrthographicCamera, HalfFloatType, FloatType, Mesh, PlaneGeometry, WebGLRenderTarget, RGBAFormat, UVMapping, WebGLRenderer, DataTexture, LinearSRGBColorSpace, ShaderMaterial, Texture, MeshBasicMaterial, IntType, ShortType, ByteType, UnsignedIntType, UnsignedByteType } from 'three';
const getBufferForType = (type, width, height) => {
let out;
switch (type) {
case UnsignedByteType:
out = new Uint8ClampedArray(width * height * 4);
break;
case HalfFloatType:
out = new Uint16Array(width * height * 4);
break;
case UnsignedIntType:
out = new Uint32Array(width * height * 4);
break;
case ByteType:
out = new Int8Array(width * height * 4);
break;
case ShortType:
out = new Int16Array(width * height * 4);
break;
case IntType:
out = new Int32Array(width * height * 4);
break;
case FloatType:
out = new Float32Array(width * height * 4);
break;
default:
throw new Error('Unsupported data type');
}
return out;
};
let _canReadPixelsResult;
/**
* Test if this browser implementation can correctly read pixels from the specified
* Render target type.
*
* Runs only once
*
* @param type
* @param renderer
* @param camera
* @param renderTargetOptions
* @returns
*/
const canReadPixels = (type, renderer, camera, renderTargetOptions) => {
if (_canReadPixelsResult !== undefined)
return _canReadPixelsResult;
const testRT = new WebGLRenderTarget(1, 1, renderTargetOptions);
renderer.setRenderTarget(testRT);
const mesh = new Mesh(new PlaneGeometry(), new MeshBasicMaterial({ color: 0xffffff }));
renderer.render(mesh, camera);
renderer.setRenderTarget(null);
const out = getBufferForType(type, testRT.width, testRT.height);
renderer.readRenderTargetPixels(testRT, 0, 0, testRT.width, testRT.height, out);
testRT.dispose();
mesh.geometry.dispose();
mesh.material.dispose();
_canReadPixelsResult = out[0] !== 0;
return _canReadPixelsResult;
};
/**
* Utility class used for rendering a texture with a material
*
* @category Core
* @group Core
*/
class QuadRenderer {
_renderer;
_rendererIsDisposable = false;
_material;
_scene;
_camera;
_quad;
_renderTarget;
_width;
_height;
_type;
_colorSpace;
_supportsReadPixels = true;
/**
* Constructs a new QuadRenderer
*
* @param options Parameters for this QuadRenderer
*/
constructor(options) {
this._width = options.width;
this._height = options.height;
this._type = options.type;
this._colorSpace = options.colorSpace;
const rtOptions = {
// fixed options
format: RGBAFormat,
depthBuffer: false,
stencilBuffer: false,
// user options
type: this._type, // set in class property
colorSpace: this._colorSpace, // set in class property
anisotropy: options.renderTargetOptions?.anisotropy !== undefined ? options.renderTargetOptions?.anisotropy : 1,
generateMipmaps: options.renderTargetOptions?.generateMipmaps !== undefined ? options.renderTargetOptions?.generateMipmaps : false,
magFilter: options.renderTargetOptions?.magFilter !== undefined ? options.renderTargetOptions?.magFilter : LinearFilter,
minFilter: options.renderTargetOptions?.minFilter !== undefined ? options.renderTargetOptions?.minFilter : LinearFilter,
samples: options.renderTargetOptions?.samples !== undefined ? options.renderTargetOptions?.samples : undefined,
wrapS: options.renderTargetOptions?.wrapS !== undefined ? options.renderTargetOptions?.wrapS : ClampToEdgeWrapping,
wrapT: options.renderTargetOptions?.wrapT !== undefined ? options.renderTargetOptions?.wrapT : ClampToEdgeWrapping
};
this._material = options.material;
if (options.renderer) {
this._renderer = options.renderer;
}
else {
this._renderer = QuadRenderer.instantiateRenderer();
this._rendererIsDisposable = true;
}
this._scene = new Scene();
this._camera = new OrthographicCamera();
this._camera.position.set(0, 0, 10);
this._camera.left = -0.5;
this._camera.right = 0.5;
this._camera.top = 0.5;
this._camera.bottom = -0.5;
this._camera.updateProjectionMatrix();
if (!canReadPixels(this._type, this._renderer, this._camera, rtOptions)) {
let alternativeType;
switch (this._type) {
case HalfFloatType:
alternativeType = this._renderer.extensions.has('EXT_color_buffer_float') ? FloatType : undefined;
break;
}
if (alternativeType !== undefined) {
console.warn(`This browser does not support reading pixels from ${this._type} RenderTargets, switching to ${FloatType}`);
this._type = alternativeType;
}
else {
this._supportsReadPixels = false;
console.warn('This browser dos not support toArray or toDataTexture, calls to those methods will result in an error thrown');
}
}
this._quad = new Mesh(new PlaneGeometry(), this._material);
this._quad.geometry.computeBoundingBox();
this._scene.add(this._quad);
this._renderTarget = new WebGLRenderTarget(this.width, this.height, rtOptions);
this._renderTarget.texture.mapping = options.renderTargetOptions?.mapping !== undefined ? options.renderTargetOptions?.mapping : UVMapping;
}
/**
* Instantiates a temporary renderer
*
* @returns
*/
static instantiateRenderer() {
const renderer = new WebGLRenderer();
renderer.setSize(128, 128);
// renderer.outputColorSpace = SRGBColorSpace
// renderer.toneMapping = LinearToneMapping
// renderer.debug.checkShaderErrors = false
// this._rendererIsDisposable = true
return renderer;
}
/**
* Renders the input texture using the specified material
*/
render = () => {
this._renderer.setRenderTarget(this._renderTarget);
try {
this._renderer.render(this._scene, this._camera);
}
catch (e) {
this._renderer.setRenderTarget(null);
throw e;
}
this._renderer.setRenderTarget(null);
};
/**
* Obtains a Buffer containing the rendered texture.
*
* @throws Error if the browser cannot read pixels from this RenderTarget type.
* @returns a TypedArray containing RGBA values from this renderer
*/
toArray() {
if (!this._supportsReadPixels)
throw new Error('Can\'t read pixels in this browser');
const out = getBufferForType(this._type, this._width, this._height);
this._renderer.readRenderTargetPixels(this._renderTarget, 0, 0, this._width, this._height, out);
return out;
}
/**
* Performs a readPixel operation in the renderTarget
* and returns a DataTexture containing the read data
*
* @param options options
* @returns
*/
toDataTexture(options) {
const returnValue = new DataTexture(
// fixed values
this.toArray(), this.width, this.height, RGBAFormat, this._type,
// user values
options?.mapping || UVMapping, options?.wrapS || ClampToEdgeWrapping, options?.wrapT || ClampToEdgeWrapping, options?.magFilter || LinearFilter, options?.minFilter || LinearFilter, options?.anisotropy || 1,
// fixed value
LinearSRGBColorSpace);
// set this afterwards, we can't set it in constructor
returnValue.generateMipmaps = options?.generateMipmaps !== undefined ? options?.generateMipmaps : false;
return returnValue;
}
/**
* If using a disposable renderer, it will dispose it.
*/
disposeOnDemandRenderer() {
this._renderer.setRenderTarget(null);
if (this._rendererIsDisposable) {
this._renderer.dispose();
this._renderer.forceContextLoss();
}
}
/**
* Will dispose of **all** assets used by this renderer.
*
*
* @param disposeRenderTarget will dispose of the renderTarget which will not be usable later
* set this to true if you passed the `renderTarget.texture` to a `PMREMGenerator`
* or are otherwise done with it.
*
* @example
* ```js
* const loader = new HDRJPGLoader(renderer)
* const result = await loader.loadAsync('gainmap.jpeg')
* const mesh = new Mesh(geometry, new MeshBasicMaterial({ map: result.renderTarget.texture }) )
* // DO NOT dispose the renderTarget here,
* // it is used directly in the material
* result.dispose()
* ```
*
* @example
* ```js
* const loader = new HDRJPGLoader(renderer)
* const pmremGenerator = new PMREMGenerator( renderer );
* const result = await loader.loadAsync('gainmap.jpeg')
* const envMap = pmremGenerator.fromEquirectangular(result.renderTarget.texture)
* const mesh = new Mesh(geometry, new MeshStandardMaterial({ envMap }) )
* // renderTarget can be disposed here
* // because it was used to generate a PMREM texture
* result.dispose(true)
* ```
*/
dispose(disposeRenderTarget) {
this.disposeOnDemandRenderer();
if (disposeRenderTarget) {
this.renderTarget.dispose();
}
// dispose shader material texture uniforms
if (this.material instanceof ShaderMaterial) {
Object.values(this.material.uniforms).forEach(v => {
if (v.value instanceof Texture)
v.value.dispose();
});
}
// dispose other material properties
Object.values(this.material).forEach(value => {
if (value instanceof Texture)
value.dispose();
});
this.material.dispose();
this._quad.geometry.dispose();
}
/**
* Width of the texture
*/
get width() { return this._width; }
set width(value) {
this._width = value;
this._renderTarget.setSize(this._width, this._height);
}
/**
* Height of the texture
*/
get height() { return this._height; }
set height(value) {
this._height = value;
this._renderTarget.setSize(this._width, this._height);
}
/**
* The renderer used
*/
get renderer() { return this._renderer; }
/**
* The `WebGLRenderTarget` used.
*/
get renderTarget() { return this._renderTarget; }
set renderTarget(value) {
this._renderTarget = value;
this._width = value.width;
this._height = value.height;
// this._type = value.texture.type
}
/**
* The `Material` used.
*/
get material() { return this._material; }
/**
*
*/
get type() { return this._type; }
get colorSpace() { return this._colorSpace; }
}
export { QuadRenderer as Q };
+5
View File
@@ -0,0 +1,5 @@
/**
* @monogrid/gainmap-js v3.4.0
* With ❤️, by MONOGRID <gainmap@monogrid.com>
*/
import{ClampToEdgeWrapping as e,LinearFilter as r,Scene as t,OrthographicCamera as i,HalfFloatType as s,FloatType as a,Mesh as n,PlaneGeometry as h,WebGLRenderTarget as o,RGBAFormat as d,UVMapping as p,WebGLRenderer as _,DataTexture as g,LinearSRGBColorSpace as l,ShaderMaterial as c,Texture as w,MeshBasicMaterial as m,IntType as T,ShortType as u,ByteType as y,UnsignedIntType as f,UnsignedByteType as O}from"three";const b=(e,r,t)=>{let i;switch(e){case O:i=new Uint8ClampedArray(r*t*4);break;case s:i=new Uint16Array(r*t*4);break;case f:i=new Uint32Array(r*t*4);break;case y:i=new Int8Array(r*t*4);break;case u:i=new Int16Array(r*t*4);break;case T:i=new Int32Array(r*t*4);break;case a:i=new Float32Array(r*t*4);break;default:throw new Error("Unsupported data type")}return i};let v;class R{_renderer;_rendererIsDisposable=!1;_material;_scene;_camera;_quad;_renderTarget;_width;_height;_type;_colorSpace;_supportsReadPixels=!0;constructor(_){this._width=_.width,this._height=_.height,this._type=_.type,this._colorSpace=_.colorSpace;const g={format:d,depthBuffer:!1,stencilBuffer:!1,type:this._type,colorSpace:this._colorSpace,anisotropy:void 0!==_.renderTargetOptions?.anisotropy?_.renderTargetOptions?.anisotropy:1,generateMipmaps:void 0!==_.renderTargetOptions?.generateMipmaps&&_.renderTargetOptions?.generateMipmaps,magFilter:void 0!==_.renderTargetOptions?.magFilter?_.renderTargetOptions?.magFilter:r,minFilter:void 0!==_.renderTargetOptions?.minFilter?_.renderTargetOptions?.minFilter:r,samples:void 0!==_.renderTargetOptions?.samples?_.renderTargetOptions?.samples:void 0,wrapS:void 0!==_.renderTargetOptions?.wrapS?_.renderTargetOptions?.wrapS:e,wrapT:void 0!==_.renderTargetOptions?.wrapT?_.renderTargetOptions?.wrapT:e};if(this._material=_.material,_.renderer?this._renderer=_.renderer:(this._renderer=R.instantiateRenderer(),this._rendererIsDisposable=!0),this._scene=new t,this._camera=new i,this._camera.position.set(0,0,10),this._camera.left=-.5,this._camera.right=.5,this._camera.top=.5,this._camera.bottom=-.5,this._camera.updateProjectionMatrix(),!((e,r,t,i)=>{if(void 0!==v)return v;const s=new o(1,1,i);r.setRenderTarget(s);const a=new n(new h,new m({color:16777215}));r.render(a,t),r.setRenderTarget(null);const d=b(e,s.width,s.height);return r.readRenderTargetPixels(s,0,0,s.width,s.height,d),s.dispose(),a.geometry.dispose(),a.material.dispose(),v=0!==d[0],v})(this._type,this._renderer,this._camera,g)){let e;if(this._type===s)e=this._renderer.extensions.has("EXT_color_buffer_float")?a:void 0;void 0!==e?(console.warn(`This browser does not support reading pixels from ${this._type} RenderTargets, switching to ${a}`),this._type=e):(this._supportsReadPixels=!1,console.warn("This browser dos not support toArray or toDataTexture, calls to those methods will result in an error thrown"))}this._quad=new n(new h,this._material),this._quad.geometry.computeBoundingBox(),this._scene.add(this._quad),this._renderTarget=new o(this.width,this.height,g),this._renderTarget.texture.mapping=void 0!==_.renderTargetOptions?.mapping?_.renderTargetOptions?.mapping:p}static instantiateRenderer(){const e=new _;return e.setSize(128,128),e}render=()=>{this._renderer.setRenderTarget(this._renderTarget);try{this._renderer.render(this._scene,this._camera)}catch(e){throw this._renderer.setRenderTarget(null),e}this._renderer.setRenderTarget(null)};toArray(){if(!this._supportsReadPixels)throw new Error("Can't read pixels in this browser");const e=b(this._type,this._width,this._height);return this._renderer.readRenderTargetPixels(this._renderTarget,0,0,this._width,this._height,e),e}toDataTexture(t){const i=new g(this.toArray(),this.width,this.height,d,this._type,t?.mapping||p,t?.wrapS||e,t?.wrapT||e,t?.magFilter||r,t?.minFilter||r,t?.anisotropy||1,l);return i.generateMipmaps=void 0!==t?.generateMipmaps&&t?.generateMipmaps,i}disposeOnDemandRenderer(){this._renderer.setRenderTarget(null),this._rendererIsDisposable&&(this._renderer.dispose(),this._renderer.forceContextLoss())}dispose(e){this.disposeOnDemandRenderer(),e&&this.renderTarget.dispose(),this.material instanceof c&&Object.values(this.material.uniforms).forEach(e=>{e.value instanceof w&&e.value.dispose()}),Object.values(this.material).forEach(e=>{e instanceof w&&e.dispose()}),this.material.dispose(),this._quad.geometry.dispose()}get width(){return this._width}set width(e){this._width=e,this._renderTarget.setSize(this._width,this._height)}get height(){return this._height}set height(e){this._height=e,this._renderTarget.setSize(this._width,this._height)}get renderer(){return this._renderer}get renderTarget(){return this._renderTarget}set renderTarget(e){this._renderTarget=e,this._width=e.width,this._height=e.height}get material(){return this._material}get type(){return this._type}get colorSpace(){return this._colorSpace}}export{R as Q};
+5
View File
@@ -0,0 +1,5 @@
/**
* @monogrid/gainmap-js v3.4.0
* With ❤️, by MONOGRID <gainmap@monogrid.com>
*/
var e="undefined"!=typeof globalThis?globalThis:"undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof self?self:{};function o(e){return e&&e.__esModule&&Object.prototype.hasOwnProperty.call(e,"default")?e.default:e}export{e as c,o as g};
+12
View File
@@ -0,0 +1,12 @@
/**
* @monogrid/gainmap-js v3.4.0
* With ❤️, by MONOGRID <gainmap@monogrid.com>
*/
var commonjsGlobal = typeof globalThis !== 'undefined' ? globalThis : typeof window !== 'undefined' ? window : typeof global !== 'undefined' ? global : typeof self !== 'undefined' ? self : {};
function getDefaultExportFromCjs (x) {
return x && x.__esModule && Object.prototype.hasOwnProperty.call(x, 'default') ? x['default'] : x;
}
export { commonjsGlobal as c, getDefaultExportFromCjs as g };
+92
View File
@@ -0,0 +1,92 @@
/**
* @monogrid/gainmap-js v3.4.0
* With ❤️, by MONOGRID <gainmap@monogrid.com>
*/
/**
* Used internally
*
* @internal
* @param canvas
* @param mimeType
* @param quality
* @returns
*/
const canvasToBlob = async (canvas, mimeType, quality) => {
if (typeof OffscreenCanvas !== 'undefined' && canvas instanceof OffscreenCanvas) {
return canvas.convertToBlob({ type: mimeType, quality: quality || 0.9 });
}
else if (canvas instanceof HTMLCanvasElement) {
return new Promise((resolve, reject) => {
canvas.toBlob((res) => {
if (res)
resolve(res);
else
reject(new Error('Failed to convert canvas to blob'));
}, mimeType, quality || 0.9);
});
}
/* istanbul ignore next
as long as this function is not exported this is only here
to satisfy TS strict mode internally
*/
throw new Error('Unsupported canvas element');
};
/**
* Converts a RAW RGBA image buffer into the provided `mimeType` using the provided `quality`
*
* @category Compression
* @group Compression
* @param params
* @throws {Error} if the browser does not support [createImageBitmap](https://caniuse.com/createimagebitmap)
* @throws {Error} if the provided source image cannot be decoded
* @throws {Error} if the function fails to create a canvas context
*/
const compress = async (params) => {
if (typeof createImageBitmap === 'undefined')
throw new Error('createImageBitmap() not supported.');
const { source, mimeType, quality, flipY } = params;
let imageBitmapSource;
if ((source instanceof Uint8Array || source instanceof Uint8ClampedArray) && 'sourceMimeType' in params) {
imageBitmapSource = new Blob([source], { type: params.sourceMimeType });
}
else if (source instanceof ImageData) {
imageBitmapSource = source;
}
else {
throw new Error('Invalid source image');
}
const img = await createImageBitmap(imageBitmapSource);
const width = img.width;
const height = img.height;
let canvas;
if (typeof OffscreenCanvas !== 'undefined') {
canvas = new OffscreenCanvas(width, height);
}
else {
canvas = document.createElement('canvas');
canvas.width = width;
canvas.height = height;
}
// apparently we do need to assert the type here for npx tsc to be happy
// eslint-disable-next-line @typescript-eslint/no-unnecessary-type-assertion
const ctx = canvas.getContext('2d');
if (!ctx)
throw new Error('Failed to create canvas Context');
// flip Y
if (flipY === true) {
ctx.translate(0, height);
ctx.scale(1, -1);
}
ctx.drawImage(img, 0, 0, width, height);
const blob = await canvasToBlob(canvas, mimeType, quality || 0.9);
const data = new Uint8Array(await blob.arrayBuffer());
return {
data,
mimeType,
width,
height
};
};
export { compress as c };
+5
View File
@@ -0,0 +1,5 @@
/**
* @monogrid/gainmap-js v3.4.0
* With ❤️, by MONOGRID <gainmap@monogrid.com>
*/
const e=async e=>{if("undefined"==typeof createImageBitmap)throw new Error("createImageBitmap() not supported.");const{source:t,mimeType:a,quality:n,flipY:r}=e;let o;if((t instanceof Uint8Array||t instanceof Uint8ClampedArray)&&"sourceMimeType"in e)o=new Blob([t],{type:e.sourceMimeType});else{if(!(t instanceof ImageData))throw new Error("Invalid source image");o=t}const i=await createImageBitmap(o),s=i.width,c=i.height;let f;"undefined"!=typeof OffscreenCanvas?f=new OffscreenCanvas(s,c):(f=document.createElement("canvas"),f.width=s,f.height=c);const m=f.getContext("2d");if(!m)throw new Error("Failed to create canvas Context");!0===r&&(m.translate(0,c),m.scale(1,-1)),m.drawImage(i,0,0,s,c);const w=await(async(e,t,a)=>{if("undefined"!=typeof OffscreenCanvas&&e instanceof OffscreenCanvas)return e.convertToBlob({type:t,quality:a||.9});if(e instanceof HTMLCanvasElement)return new Promise((n,r)=>{e.toBlob(e=>{e?n(e):r(new Error("Failed to convert canvas to blob"))},t,a||.9)});throw new Error("Unsupported canvas element")})(f,a,n||.9);return{data:new Uint8Array(await w.arrayBuffer()),mimeType:a,width:s,height:c}};export{e as c};
+153
View File
@@ -0,0 +1,153 @@
import { ByteType, ColorSpace, DataTexture, FloatType, HalfFloatType, IntType, Material, ShortType, Texture, TextureDataType, UnsignedByteType, UnsignedInt248Type, UnsignedInt5999Type, UnsignedInt101111Type, UnsignedIntType, UnsignedShort4444Type, UnsignedShort5551Type, UnsignedShortType, WebGLRenderer, WebGLRenderTarget } from 'three';
import { QuadRendererTextureOptions, TextureImageFormat } from './types';
/**
* Utility Type that translates `three` texture types to their TypedArray counterparts.
*
* @category Utility
* @group Utility
*/
export type TextureDataTypeToBufferType<TType extends TextureDataType> = TType extends typeof UnsignedByteType ? Uint8ClampedArray<ArrayBuffer> : TType extends typeof ByteType ? Int8Array<ArrayBuffer> : TType extends typeof ShortType ? Int16Array<ArrayBuffer> : TType extends typeof UnsignedShortType ? Uint16Array<ArrayBuffer> : TType extends typeof IntType ? Int32Array<ArrayBuffer> : TType extends typeof UnsignedIntType ? Uint32Array<ArrayBuffer> : TType extends typeof FloatType ? Float32Array<ArrayBuffer> : TType extends typeof HalfFloatType ? Uint16Array<ArrayBuffer> : TType extends typeof UnsignedShort4444Type ? Uint16Array<ArrayBuffer> : TType extends typeof UnsignedShort5551Type ? Uint16Array<ArrayBuffer> : TType extends typeof UnsignedInt248Type ? Uint32Array<ArrayBuffer> : TType extends typeof UnsignedInt5999Type ? Uint32Array<ArrayBuffer> : TType extends typeof UnsignedInt101111Type ? Uint32Array<ArrayBuffer> : never;
export type QuadRendererOptions<TType extends TextureDataType, TMaterial extends Material> = {
/**
* Width of the render target
*/
width: number;
/**
* height of the renderTarget
*/
height: number;
/**
* TextureDataType of the renderTarget
*/
type: TType;
/**
* ColorSpace of the renderTarget
*/
colorSpace: ColorSpace;
/**
* material to use for rendering
*/
material: TMaterial;
/**
* Renderer instance to use
*/
renderer?: WebGLRenderer;
/**
* Additional renderTarget options
*/
renderTargetOptions?: QuadRendererTextureOptions;
};
/**
* Utility class used for rendering a texture with a material
*
* @category Core
* @group Core
*/
export declare class QuadRenderer<TType extends TextureDataType, TMaterial extends Material> {
private _renderer;
private _rendererIsDisposable;
private _material;
private _scene;
private _camera;
private _quad;
private _renderTarget;
private _width;
private _height;
private _type;
private _colorSpace;
private _supportsReadPixels;
/**
* Constructs a new QuadRenderer
*
* @param options Parameters for this QuadRenderer
*/
constructor(options: QuadRendererOptions<TType, TMaterial>);
/**
* Instantiates a temporary renderer
*
* @returns
*/
static instantiateRenderer(): WebGLRenderer;
/**
* Renders the input texture using the specified material
*/
render: () => void;
/**
* Obtains a Buffer containing the rendered texture.
*
* @throws Error if the browser cannot read pixels from this RenderTarget type.
* @returns a TypedArray containing RGBA values from this renderer
*/
toArray(): TextureDataTypeToBufferType<TType>;
/**
* Performs a readPixel operation in the renderTarget
* and returns a DataTexture containing the read data
*
* @param options options
* @returns
*/
toDataTexture(options?: QuadRendererTextureOptions): DataTexture;
/**
* If using a disposable renderer, it will dispose it.
*/
disposeOnDemandRenderer(): void;
/**
* Will dispose of **all** assets used by this renderer.
*
*
* @param disposeRenderTarget will dispose of the renderTarget which will not be usable later
* set this to true if you passed the `renderTarget.texture` to a `PMREMGenerator`
* or are otherwise done with it.
*
* @example
* ```js
* const loader = new HDRJPGLoader(renderer)
* const result = await loader.loadAsync('gainmap.jpeg')
* const mesh = new Mesh(geometry, new MeshBasicMaterial({ map: result.renderTarget.texture }) )
* // DO NOT dispose the renderTarget here,
* // it is used directly in the material
* result.dispose()
* ```
*
* @example
* ```js
* const loader = new HDRJPGLoader(renderer)
* const pmremGenerator = new PMREMGenerator( renderer );
* const result = await loader.loadAsync('gainmap.jpeg')
* const envMap = pmremGenerator.fromEquirectangular(result.renderTarget.texture)
* const mesh = new Mesh(geometry, new MeshStandardMaterial({ envMap }) )
* // renderTarget can be disposed here
* // because it was used to generate a PMREM texture
* result.dispose(true)
* ```
*/
dispose(disposeRenderTarget?: boolean): void;
/**
* Width of the texture
*/
get width(): number;
set width(value: number);
/**
* Height of the texture
*/
get height(): number;
set height(value: number);
/**
* The renderer used
*/
get renderer(): WebGLRenderer;
/**
* The `WebGLRenderTarget` used.
*/
get renderTarget(): WebGLRenderTarget<Texture<TextureImageFormat>>;
set renderTarget(value: WebGLRenderTarget<Texture<TextureImageFormat>>);
/**
* The `Material` used.
*/
get material(): TMaterial;
/**
*
*/
get type(): TType;
get colorSpace(): ColorSpace;
}
+14
View File
@@ -0,0 +1,14 @@
import { DataTexture } from 'three';
import { EXR } from 'three/examples/jsm/loaders/EXRLoader.js';
import { HDR } from 'three/examples/jsm/loaders/HDRLoader.js';
import { RGBE } from 'three/examples/jsm/loaders/RGBELoader.js';
/**
* Utility function to obtain a `DataTexture` from various input formats
*
* @category Utility
* @group Utility
*
* @param image
* @returns
*/
export declare const getDataTexture: (image: EXR | RGBE | HDR | DataTexture) => DataTexture;
+3
View File
@@ -0,0 +1,3 @@
export * from './get-data-texture';
export * from './QuadRenderer';
export * from './types';
+93
View File
@@ -0,0 +1,93 @@
import { Mapping, RenderTargetOptions } from 'three';
export type Constructor = new (...args: any[]) => any;
/**
* This is the Metadata stored in an encoded Gainmap which is used
* to decode it and return an HDR image
*
* @category Specs
* @group Specs
*/
export type GainMapMetadata = {
/**
* This is the gamma to apply to the stored map values.
* @defaultValue [1, 1, 1]
* @remarks
* * Typically you can use a gamma of 1.0.
* * You can use a different value if your gain map has a very uneven distribution of log_recovery(x, y) values.
*
* 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),
* 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).
* 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).
*/
gamma: [number, number, number];
/**
* This is log2 of the minimum display boost value for which the map is applied at all.
*
* @remarks
* * This value also affects how much to apply the gain map based on the display boost.
* * Must be 0.0 or greater.
*
* Logarithmic space
*/
hdrCapacityMin: number;
/**
* Stores the value of hdr_capacity_max. This is log2 of the maximum display boost value for which the map is applied completely.
*
* @remarks
* * This value also affects how much to apply the gain map based on the display boost.
* * Must be greater than hdrCapacityMin.
* * Required.
*
* Logarithmic space
*/
hdrCapacityMax: number;
/**
* This is the offset to apply to the SDR pixel values during gain map generation and application
* @defaultValue [1/64, 1/64, 1/64]
*/
offsetSdr: [number, number, number];
/**
* This is the offset to apply to the HDR pixel values during gain map generation and application.
* @defaultValue [1/64, 1/64, 1/64]
*/
offsetHdr: [number, number, number];
/**
* This is log2 of min content boost, which is the minimum allowed ratio of
* the linear luminance for the target HDR rendition relative to
* (divided by) that of the SDR image, at a given pixel.
*/
gainMapMin: [number, number, number];
/**
* This is log2 of max content boost, which is the maximum allowed ratio of
* the linear luminance for the Target HDR rendition relative to
* (divided by) that of the SDR image, at a given pixel.
*/
gainMapMax: [number, number, number];
};
export type GainMapMetadataExtended = GainMapMetadata & {
version: string;
maxContentBoost: number;
minContentBoost: number;
};
/**
*
*/
export type QuadRendererTextureOptions = Omit<RenderTargetOptions, 'type' | 'format' | 'colorSpace' | 'encoding' | 'depthTexture' | 'stencilBuffer' | 'depthBuffer' | 'internalFormat'> & {
/**
* @defaultValue 300
*/
mapping?: Mapping;
/**
* @defaultValue 1
*/
anisotropy?: number;
};
/**
* Accepted image format for textures.
* For many operations we need a texture based on an image with width & height.
* It could also be a canvas, the only thing we actually need is a width and a height
*/
export type TextureImageFormat = {
width: number;
height: number;
};
+1
View File
@@ -0,0 +1 @@
export * from './decode/index';
+574
View File
@@ -0,0 +1,574 @@
/**
* @monogrid/gainmap-js v3.4.0
* With ❤️, by MONOGRID <gainmap@monogrid.com>
*/
import { Q as QuadRenderer } from './QuadRenderer-Bj1xl_EK.js';
import { c as createDecodeFunction, L as LoaderBaseShared, e as extractGainmapFromJPEG, X as XMPMetadataNotFoundError, G as GainMapNotFoundError } from './Loader-DLI-_JDP.js';
export { M as MPFExtractor, a as extractXMP } from './Loader-DLI-_JDP.js';
import { ShaderMaterial, NoBlending, Vector3, WebGLRenderer, FileLoader } from 'three';
const vertexShader = /* glsl */ `
varying vec2 vUv;
void main() {
vUv = uv;
gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
}
`;
const fragmentShader = /* glsl */ `
// min half float value
#define HALF_FLOAT_MIN vec3( -65504, -65504, -65504 )
// max half float value
#define HALF_FLOAT_MAX vec3( 65504, 65504, 65504 )
uniform sampler2D sdr;
uniform sampler2D gainMap;
uniform vec3 gamma;
uniform vec3 offsetHdr;
uniform vec3 offsetSdr;
uniform vec3 gainMapMin;
uniform vec3 gainMapMax;
uniform float weightFactor;
varying vec2 vUv;
void main() {
vec3 rgb = texture2D( sdr, vUv ).rgb;
vec3 recovery = texture2D( gainMap, vUv ).rgb;
vec3 logRecovery = pow( recovery, gamma );
vec3 logBoost = gainMapMin * ( 1.0 - logRecovery ) + gainMapMax * logRecovery;
vec3 hdrColor = (rgb + offsetSdr) * exp2( logBoost * weightFactor ) - offsetHdr;
vec3 clampedHdrColor = max( HALF_FLOAT_MIN, min( HALF_FLOAT_MAX, hdrColor ));
gl_FragColor = vec4( clampedHdrColor , 1.0 );
}
`;
/**
* A Material which is able to decode the Gainmap into a full HDR Representation
*
* @category Materials
* @group Materials
*/
class GainMapDecoderMaterial extends ShaderMaterial {
_maxDisplayBoost;
_hdrCapacityMin;
_hdrCapacityMax;
/**
*
* @param params
*/
constructor({ gamma, offsetHdr, offsetSdr, gainMapMin, gainMapMax, maxDisplayBoost, hdrCapacityMin, hdrCapacityMax, sdr, gainMap }) {
super({
name: 'GainMapDecoderMaterial',
vertexShader,
fragmentShader,
uniforms: {
sdr: { value: sdr },
gainMap: { value: gainMap },
gamma: { value: new Vector3(1.0 / gamma[0], 1.0 / gamma[1], 1.0 / gamma[2]) },
offsetHdr: { value: new Vector3().fromArray(offsetHdr) },
offsetSdr: { value: new Vector3().fromArray(offsetSdr) },
gainMapMin: { value: new Vector3().fromArray(gainMapMin) },
gainMapMax: { value: new Vector3().fromArray(gainMapMax) },
weightFactor: {
value: (Math.log2(maxDisplayBoost) - hdrCapacityMin) / (hdrCapacityMax - hdrCapacityMin)
}
},
blending: NoBlending,
depthTest: false,
depthWrite: false
});
this._maxDisplayBoost = maxDisplayBoost;
this._hdrCapacityMin = hdrCapacityMin;
this._hdrCapacityMax = hdrCapacityMax;
this.needsUpdate = true;
this.uniformsNeedUpdate = true;
}
get sdr() { return this.uniforms.sdr.value; }
set sdr(value) { this.uniforms.sdr.value = value; }
get gainMap() { return this.uniforms.gainMap.value; }
set gainMap(value) { this.uniforms.gainMap.value = value; }
/**
* @see {@link GainMapMetadata.offsetHdr}
*/
get offsetHdr() { return this.uniforms.offsetHdr.value.toArray(); }
set offsetHdr(value) { this.uniforms.offsetHdr.value.fromArray(value); }
/**
* @see {@link GainMapMetadata.offsetSdr}
*/
get offsetSdr() { return this.uniforms.offsetSdr.value.toArray(); }
set offsetSdr(value) { this.uniforms.offsetSdr.value.fromArray(value); }
/**
* @see {@link GainMapMetadata.gainMapMin}
*/
get gainMapMin() { return this.uniforms.gainMapMin.value.toArray(); }
set gainMapMin(value) { this.uniforms.gainMapMin.value.fromArray(value); }
/**
* @see {@link GainMapMetadata.gainMapMax}
*/
get gainMapMax() { return this.uniforms.gainMapMax.value.toArray(); }
set gainMapMax(value) { this.uniforms.gainMapMax.value.fromArray(value); }
/**
* @see {@link GainMapMetadata.gamma}
*/
get gamma() {
const g = this.uniforms.gamma.value;
return [1 / g.x, 1 / g.y, 1 / g.z];
}
set gamma(value) {
const g = this.uniforms.gamma.value;
g.x = 1.0 / value[0];
g.y = 1.0 / value[1];
g.z = 1.0 / value[2];
}
/**
* @see {@link GainMapMetadata.hdrCapacityMin}
* @remarks Logarithmic space
*/
get hdrCapacityMin() { return this._hdrCapacityMin; }
set hdrCapacityMin(value) {
this._hdrCapacityMin = value;
this.calculateWeight();
}
/**
* @see {@link GainMapMetadata.hdrCapacityMin}
* @remarks Logarithmic space
*/
get hdrCapacityMax() { return this._hdrCapacityMax; }
set hdrCapacityMax(value) {
this._hdrCapacityMax = value;
this.calculateWeight();
}
/**
* @see {@link GainmapDecodingParameters.maxDisplayBoost}
* @remarks Non Logarithmic space
*/
get maxDisplayBoost() { return this._maxDisplayBoost; }
set maxDisplayBoost(value) {
this._maxDisplayBoost = Math.max(1, Math.min(65504, value));
this.calculateWeight();
}
calculateWeight() {
const val = (Math.log2(this._maxDisplayBoost) - this._hdrCapacityMin) / (this._hdrCapacityMax - this._hdrCapacityMin);
this.uniforms.weightFactor.value = Math.max(0, Math.min(1, val));
}
}
const decodeImpl = createDecodeFunction({
renderer: WebGLRenderer,
createMaterial: (params) => new GainMapDecoderMaterial(params),
createQuadRenderer: (params) => new QuadRenderer(params)
});
/**
* Decodes a gain map using a WebGL RenderTarget
*
* @category Decoding Functions
* @group Decoding Functions
* @example
* import { decode } from '@monogrid/gainmap-js'
* import {
* Mesh,
* MeshBasicMaterial,
* PerspectiveCamera,
* PlaneGeometry,
* Scene,
* TextureLoader,
* WebGLRenderer
* } from 'three'
*
* const renderer = new WebGLRenderer()
*
* const textureLoader = new TextureLoader()
*
* // load SDR Representation
* const sdr = await textureLoader.loadAsync('sdr.jpg')
* // load Gain map recovery image
* const gainMap = await textureLoader.loadAsync('gainmap.jpg')
* // load metadata
* const metadata = await (await fetch('metadata.json')).json()
*
* const result = decode({
* sdr,
* gainMap,
* // this allows to use `result.renderTarget.texture` directly
* renderer,
* // this will restore the full HDR range
* maxDisplayBoost: Math.pow(2, metadata.hdrCapacityMax),
* ...metadata
* })
*
* const scene = new Scene()
* // `result` can be used to populate a Texture
* const mesh = new Mesh(
* new PlaneGeometry(),
* new MeshBasicMaterial({ map: result.renderTarget.texture })
* )
* scene.add(mesh)
* renderer.render(scene, new PerspectiveCamera())
*
* // result must be manually disposed
* // when you are done using it
* result.dispose()
*
* @param params
* @returns
* @throws {Error} if the WebGLRenderer fails to render the gain map
*/
const decode = (params) => {
// Ensure renderer is defined for the base function
if (!params.renderer) {
throw new Error('Renderer is required for decode function');
}
const quadRenderer = decodeImpl({
...params,
renderer: params.renderer
});
try {
quadRenderer.render();
}
catch (e) {
quadRenderer.disposeOnDemandRenderer();
throw e;
}
return quadRenderer;
};
/**
* Base class for WebGL loaders
* @template TUrl - The type of URL used to load resources
*/
class LoaderBaseWebGL extends LoaderBaseShared {
constructor(renderer, manager) {
super({
renderer,
createMaterial: (params) => new GainMapDecoderMaterial(params),
createQuadRenderer: (params) => new QuadRenderer(params)
}, manager);
}
/**
* @private
* @param quadRenderer
* @param metadata
* @param sdrBuffer
* @param gainMapBuffer
*/
async render(quadRenderer, metadata, sdrBuffer, gainMapBuffer) {
const { sdrImage, gainMapImage, needsFlip } = await this.processImages(sdrBuffer, gainMapBuffer, 'flipY');
const { gainMap, sdr } = this.createTextures(sdrImage, gainMapImage, needsFlip);
this.updateQuadRenderer(quadRenderer, sdrImage, gainMap, sdr, metadata);
quadRenderer.render();
}
}
/**
* A Three.js Loader for the gain map format.
*
* @category Loaders
* @group Loaders
*
* @example
* import { GainMapLoader } from '@monogrid/gainmap-js'
* import {
* EquirectangularReflectionMapping,
* Mesh,
* MeshBasicMaterial,
* PerspectiveCamera,
* PlaneGeometry,
* Scene,
* WebGLRenderer
* } from 'three'
*
* const renderer = new WebGLRenderer()
*
* const loader = new GainMapLoader(renderer)
* .setRenderTargetOptions({ mapping: EquirectangularReflectionMapping })
*
* const result = await loader.loadAsync(['sdr.jpeg', 'gainmap.jpeg', 'metadata.json'])
* // `result` can be used to populate a Texture
*
* const scene = new Scene()
* const mesh = new Mesh(
* new PlaneGeometry(),
* new MeshBasicMaterial({ map: result.renderTarget.texture })
* )
* scene.add(mesh)
* renderer.render(scene, new PerspectiveCamera())
*
* // Starting from three.js r159
* // `result.renderTarget.texture` can
* // also be used as Equirectangular scene background
* //
* // it was previously needed to convert it
* // to a DataTexture with `result.toDataTexture()`
* scene.background = result.renderTarget.texture
*
* // result must be manually disposed
* // when you are done using it
* result.dispose()
*
*/
class GainMapLoader extends LoaderBaseWebGL {
/**
* Loads a gainmap using separate data
* * sdr image
* * gain map image
* * metadata json
*
* useful for webp gain maps
*
* @param urls An array in the form of [sdr.jpg, gainmap.jpg, metadata.json]
* @param onLoad Load complete callback, will receive the result
* @param onProgress Progress callback, will receive a `ProgressEvent`
* @param onError Error callback
* @returns
*/
load([sdrUrl, gainMapUrl, metadataUrl], onLoad, onProgress, onError) {
const quadRenderer = this.prepareQuadRenderer();
let sdr;
let gainMap;
let metadata;
const loadCheck = async () => {
if (sdr && gainMap && metadata) {
// solves #16
try {
await this.render(quadRenderer, metadata, sdr, gainMap);
}
catch (error) {
this.manager.itemError(sdrUrl);
this.manager.itemError(gainMapUrl);
this.manager.itemError(metadataUrl);
if (typeof onError === 'function')
onError(error);
quadRenderer.disposeOnDemandRenderer();
return;
}
if (typeof onLoad === 'function')
onLoad(quadRenderer);
this.manager.itemEnd(sdrUrl);
this.manager.itemEnd(gainMapUrl);
this.manager.itemEnd(metadataUrl);
quadRenderer.disposeOnDemandRenderer();
}
};
let sdrLengthComputable = true;
let sdrTotal = 0;
let sdrLoaded = 0;
let gainMapLengthComputable = true;
let gainMapTotal = 0;
let gainMapLoaded = 0;
let metadataLengthComputable = true;
let metadataTotal = 0;
let metadataLoaded = 0;
const progressHandler = () => {
if (typeof onProgress === 'function') {
const total = sdrTotal + gainMapTotal + metadataTotal;
const loaded = sdrLoaded + gainMapLoaded + metadataLoaded;
const lengthComputable = sdrLengthComputable && gainMapLengthComputable && metadataLengthComputable;
onProgress(new ProgressEvent('progress', { lengthComputable, loaded, total }));
}
};
this.manager.itemStart(sdrUrl);
this.manager.itemStart(gainMapUrl);
this.manager.itemStart(metadataUrl);
const sdrLoader = new FileLoader(this._internalLoadingManager);
sdrLoader.setResponseType('arraybuffer');
sdrLoader.setRequestHeader(this.requestHeader);
sdrLoader.setPath(this.path);
sdrLoader.setWithCredentials(this.withCredentials);
sdrLoader.load(sdrUrl, async (buffer) => {
/* istanbul ignore if
this condition exists only because of three.js types + strict mode
*/
if (typeof buffer === 'string')
throw new Error('Invalid sdr buffer');
sdr = buffer;
await loadCheck();
}, (e) => {
sdrLengthComputable = e.lengthComputable;
sdrLoaded = e.loaded;
sdrTotal = e.total;
progressHandler();
}, (error) => {
this.manager.itemError(sdrUrl);
if (typeof onError === 'function')
onError(error);
});
const gainMapLoader = new FileLoader(this._internalLoadingManager);
gainMapLoader.setResponseType('arraybuffer');
gainMapLoader.setRequestHeader(this.requestHeader);
gainMapLoader.setPath(this.path);
gainMapLoader.setWithCredentials(this.withCredentials);
gainMapLoader.load(gainMapUrl, async (buffer) => {
/* istanbul ignore if
this condition exists only because of three.js types + strict mode
*/
if (typeof buffer === 'string')
throw new Error('Invalid gainmap buffer');
gainMap = buffer;
await loadCheck();
}, (e) => {
gainMapLengthComputable = e.lengthComputable;
gainMapLoaded = e.loaded;
gainMapTotal = e.total;
progressHandler();
}, (error) => {
this.manager.itemError(gainMapUrl);
if (typeof onError === 'function')
onError(error);
});
const metadataLoader = new FileLoader(this._internalLoadingManager);
// metadataLoader.setResponseType('json')
metadataLoader.setRequestHeader(this.requestHeader);
metadataLoader.setPath(this.path);
metadataLoader.setWithCredentials(this.withCredentials);
metadataLoader.load(metadataUrl, async (json) => {
/* istanbul ignore if
this condition exists only because of three.js types + strict mode
*/
if (typeof json !== 'string')
throw new Error('Invalid metadata string');
// TODO: implement check on JSON file and remove this eslint disable
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
metadata = JSON.parse(json);
await loadCheck();
}, (e) => {
metadataLengthComputable = e.lengthComputable;
metadataLoaded = e.loaded;
metadataTotal = e.total;
progressHandler();
}, (error) => {
this.manager.itemError(metadataUrl);
if (typeof onError === 'function')
onError(error);
});
return quadRenderer;
}
}
/**
* A Three.js Loader for a JPEG with embedded gainmap metadata.
*
* @category Loaders
* @group Loaders
*
* @example
* import { HDRJPGLoader } from '@monogrid/gainmap-js'
* import {
* EquirectangularReflectionMapping,
* Mesh,
* MeshBasicMaterial,
* PerspectiveCamera,
* PlaneGeometry,
* Scene,
* WebGLRenderer
* } from 'three'
*
* const renderer = new WebGLRenderer()
*
* const loader = new HDRJPGLoader(renderer)
* .setRenderTargetOptions({ mapping: EquirectangularReflectionMapping })
*
* const result = await loader.loadAsync('gainmap.jpeg')
* // `result` can be used to populate a Texture
*
* const scene = new Scene()
* const mesh = new Mesh(
* new PlaneGeometry(),
* new MeshBasicMaterial({ map: result.renderTarget.texture })
* )
* scene.add(mesh)
* renderer.render(scene, new PerspectiveCamera())
*
* // Starting from three.js r159
* // `result.renderTarget.texture` can
* // also be used as Equirectangular scene background
* //
* // it was previously needed to convert it
* // to a DataTexture with `result.toDataTexture()`
* scene.background = result.renderTarget.texture
*
* // result must be manually disposed
* // when you are done using it
* result.dispose()
*
*/
class HDRJPGLoader extends LoaderBaseWebGL {
/**
* Loads a JPEG containing gain map metadata
* Renders a normal SDR image if gainmap data is not found
*
* @param url Path to a JPEG file containing embedded gain map metadata
* @param onLoad Load complete callback, will receive the result
* @param onProgress Progress callback, will receive a `ProgressEvent`
* @param onError Error callback
* @returns
*/
load(url, onLoad, onProgress, onError) {
const quadRenderer = this.prepareQuadRenderer();
const loader = new FileLoader(this._internalLoadingManager);
loader.setResponseType('arraybuffer');
loader.setRequestHeader(this.requestHeader);
loader.setPath(this.path);
loader.setWithCredentials(this.withCredentials);
this.manager.itemStart(url);
loader.load(url, async (jpeg) => {
/* istanbul ignore if
this condition exists only because of three.js types + strict mode
*/
if (typeof jpeg === 'string')
throw new Error('Invalid buffer, received [string], was expecting [ArrayBuffer]');
const jpegBuffer = new Uint8Array(jpeg);
let sdrJPEG;
let gainMapJPEG;
let metadata;
try {
const extractionResult = await extractGainmapFromJPEG(jpegBuffer);
// gain map is successfully reconstructed
sdrJPEG = extractionResult.sdr;
gainMapJPEG = extractionResult.gainMap;
metadata = extractionResult.metadata;
}
catch (e) {
// render the SDR version if this is not a gainmap
if (e instanceof XMPMetadataNotFoundError || e instanceof GainMapNotFoundError) {
console.warn(`Failure to reconstruct an HDR image from ${url}: Gain map metadata not found in the file, HDRJPGLoader will render the SDR jpeg`);
metadata = {
gainMapMin: [0, 0, 0],
gainMapMax: [1, 1, 1],
gamma: [1, 1, 1],
hdrCapacityMin: 0,
hdrCapacityMax: 1,
offsetHdr: [0, 0, 0],
offsetSdr: [0, 0, 0]
};
sdrJPEG = jpegBuffer;
}
else {
throw e;
}
}
// solves #16
try {
await this.render(quadRenderer, metadata, sdrJPEG.buffer, gainMapJPEG?.buffer);
}
catch (error) {
this.manager.itemError(url);
if (typeof onError === 'function')
onError(error);
quadRenderer.disposeOnDemandRenderer();
return;
}
if (typeof onLoad === 'function')
onLoad(quadRenderer);
this.manager.itemEnd(url);
quadRenderer.disposeOnDemandRenderer();
}, onProgress, (error) => {
this.manager.itemError(url);
if (typeof onError === 'function')
onError(error);
});
return quadRenderer;
}
}
export { GainMapDecoderMaterial, GainMapLoader, GainMapNotFoundError, HDRJPGLoader, HDRJPGLoader as JPEGRLoader, LoaderBaseShared, QuadRenderer, XMPMetadataNotFoundError, createDecodeFunction, decode, extractGainmapFromJPEG };
File diff suppressed because one or more lines are too long
File diff suppressed because it is too large Load Diff
File diff suppressed because one or more lines are too long
+814
View File
@@ -0,0 +1,814 @@
/**
* @monogrid/gainmap-js v3.4.0
* With ❤️, by MONOGRID <gainmap@monogrid.com>
*/
import { c as createDecodeFunction, L as LoaderBaseShared, e as extractGainmapFromJPEG, X as XMPMetadataNotFoundError, G as GainMapNotFoundError } from './Loader-DLI-_JDP.js';
export { M as MPFExtractor, a as extractXMP } from './Loader-DLI-_JDP.js';
import { ClampToEdgeWrapping, LinearFilter, Scene, OrthographicCamera, Mesh, PlaneGeometry, RenderTarget, RGBAFormat, UVMapping, WebGPURenderer, DataTexture, LinearSRGBColorSpace, ShaderMaterial, Texture, MeshBasicNodeMaterial, NoBlending, FileLoader } from 'three/webgpu';
import 'three';
import { vec3, texture, uniform, pow, sub, float, add, mul, exp2, max, min } from 'three/tsl';
/**
* Utility class used for rendering a texture with a material (WebGPU version)
*
* @category Core
* @group Core
*/
class QuadRenderer {
_renderer;
_rendererIsDisposable = false;
_material;
_scene;
_camera;
_quad;
_renderTarget;
_width;
_height;
_type;
_colorSpace;
_supportsReadPixels = true;
/**
* Constructs a new QuadRenderer
*
* @param options Parameters for this QuadRenderer
*/
constructor(options) {
this._width = options.width;
this._height = options.height;
this._type = options.type;
this._colorSpace = options.colorSpace;
const rtOptions = {
// fixed options
format: RGBAFormat,
depthBuffer: false,
stencilBuffer: false,
// user options
type: this._type, // set in class property
colorSpace: this._colorSpace, // set in class property
anisotropy: options.renderTargetOptions?.anisotropy !== undefined ? options.renderTargetOptions?.anisotropy : 1,
generateMipmaps: options.renderTargetOptions?.generateMipmaps !== undefined ? options.renderTargetOptions?.generateMipmaps : false,
magFilter: options.renderTargetOptions?.magFilter !== undefined ? options.renderTargetOptions?.magFilter : LinearFilter,
minFilter: options.renderTargetOptions?.minFilter !== undefined ? options.renderTargetOptions?.minFilter : LinearFilter,
samples: options.renderTargetOptions?.samples !== undefined ? options.renderTargetOptions?.samples : undefined,
wrapS: options.renderTargetOptions?.wrapS !== undefined ? options.renderTargetOptions?.wrapS : ClampToEdgeWrapping,
wrapT: options.renderTargetOptions?.wrapT !== undefined ? options.renderTargetOptions?.wrapT : ClampToEdgeWrapping
};
this._material = options.material;
if (options.renderer) {
this._renderer = options.renderer;
}
else {
this._renderer = QuadRenderer.instantiateRenderer();
this._rendererIsDisposable = true;
}
this._scene = new Scene();
this._camera = new OrthographicCamera();
this._camera.position.set(0, 0, 10);
this._camera.left = -0.5;
this._camera.right = 0.5;
this._camera.top = 0.5;
this._camera.bottom = -0.5;
this._camera.updateProjectionMatrix();
this._quad = new Mesh(new PlaneGeometry(), this._material);
this._quad.geometry.computeBoundingBox();
this._scene.add(this._quad);
this._renderTarget = new RenderTarget(this.width, this.height, rtOptions);
this._renderTarget.texture.mapping = options.renderTargetOptions?.mapping !== undefined ? options.renderTargetOptions?.mapping : UVMapping;
}
/**
* Instantiates a temporary renderer
*
* @returns
*/
static instantiateRenderer() {
const renderer = new WebGPURenderer();
renderer.setSize(128, 128);
return renderer;
}
/**
* Renders the input texture using the specified material
*/
render = async () => {
if (!this._renderer.hasInitialized()) {
await this._renderer.init();
}
this._renderer.setRenderTarget(this._renderTarget);
try {
this._renderer.render(this._scene, this._camera);
}
catch (e) {
this._renderer.setRenderTarget(null);
throw e;
}
this._renderer.setRenderTarget(null);
};
/**
* Obtains a Buffer containing the rendered texture.
*
* @throws Error if the browser cannot read pixels from this RenderTarget type.
* @returns a TypedArray containing RGBA values from this renderer
*/
async toArray() {
if (!this._supportsReadPixels)
throw new Error('Can\'t read pixels in this browser');
const out = await this._renderer.readRenderTargetPixelsAsync(this._renderTarget, 0, 0, this._width, this._height);
return out;
}
/**
* Performs a readPixel operation in the renderTarget
* and returns a DataTexture containing the read data
*
* @param options options
* @returns
*/
async toDataTexture(options) {
const returnValue = new DataTexture(
// fixed values
await this.toArray(), this.width, this.height, RGBAFormat, this._type,
// user values
options?.mapping || UVMapping, options?.wrapS || ClampToEdgeWrapping, options?.wrapT || ClampToEdgeWrapping, options?.magFilter || LinearFilter, options?.minFilter || LinearFilter, options?.anisotropy || 1,
// fixed value
LinearSRGBColorSpace);
returnValue.flipY = options?.flipY !== undefined ? options?.flipY : true;
// set this afterwards, we can't set it in constructor
returnValue.generateMipmaps = options?.generateMipmaps !== undefined ? options?.generateMipmaps : false;
return returnValue;
}
/**
* If using a disposable renderer, it will dispose it.
*/
disposeOnDemandRenderer() {
this._renderer.setRenderTarget(null);
if (this._rendererIsDisposable) {
this._renderer.dispose();
}
}
/**
* Will dispose of **all** assets used by this renderer.
*
*
* @param disposeRenderTarget will dispose of the renderTarget which will not be usable later
* set this to true if you passed the `renderTarget.texture` to a `PMREMGenerator`
* or are otherwise done with it.
*
* @example
* ```js
* const loader = new HDRJPGLoader(renderer)
* const result = await loader.loadAsync('gainmap.jpeg')
* const mesh = new Mesh(geometry, new MeshBasicMaterial({ map: result.renderTarget.texture }) )
* // DO NOT dispose the renderTarget here,
* // it is used directly in the material
* result.dispose()
* ```
*
* @example
* ```js
* const loader = new HDRJPGLoader(renderer)
* const pmremGenerator = new PMREMGenerator( renderer );
* const result = await loader.loadAsync('gainmap.jpeg')
* const envMap = pmremGenerator.fromEquirectangular(result.renderTarget.texture)
* const mesh = new Mesh(geometry, new MeshStandardMaterial({ envMap }) )
* // renderTarget can be disposed here
* // because it was used to generate a PMREM texture
* result.dispose(true)
* ```
*/
dispose(disposeRenderTarget) {
if (disposeRenderTarget) {
this.renderTarget.dispose();
}
// dispose shader material texture uniforms
if (this.material instanceof ShaderMaterial) {
Object.values(this.material.uniforms).forEach(v => {
if (v.value instanceof Texture)
v.value.dispose();
});
}
// dispose other material properties
Object.values(this.material).forEach(value => {
if (value instanceof Texture)
value.dispose();
});
this.material.dispose();
this._quad.geometry.dispose();
this.disposeOnDemandRenderer();
}
/**
* Width of the texture
*/
get width() { return this._width; }
set width(value) {
this._width = value;
this._renderTarget.setSize(this._width, this._height);
}
/**
* Height of the texture
*/
get height() { return this._height; }
set height(value) {
this._height = value;
this._renderTarget.setSize(this._width, this._height);
}
/**
* The renderer used
*/
get renderer() { return this._renderer; }
/**
* The `RenderTarget` used.
*/
get renderTarget() { return this._renderTarget; }
set renderTarget(value) {
this._renderTarget = value;
this._width = value.width;
this._height = value.height;
}
/**
* The `Material` used.
*/
get material() { return this._material; }
/**
*
*/
get type() { return this._type; }
get colorSpace() { return this._colorSpace; }
}
// min half float value
const HALF_FLOAT_MIN = vec3(-65504, -65504, -65504);
// max half float value
const HALF_FLOAT_MAX = vec3(65504, 65504, 65504);
/**
* A Material which is able to decode the Gainmap into a full HDR Representation using TSL (Three.js Shading Language)
*
* @category Materials
* @group Materials
*/
class GainMapDecoderMaterial extends MeshBasicNodeMaterial {
_maxDisplayBoost;
_hdrCapacityMin;
_hdrCapacityMax;
// Uniforms for TSL
_gammaUniform;
_offsetHdrUniform;
_offsetSdrUniform;
_gainMapMinUniform;
_gainMapMaxUniform;
_weightFactorUniform;
_sdrTexture;
_gainMapTexture;
/**
*
* @param params
*/
constructor({ gamma, offsetHdr, offsetSdr, gainMapMin, gainMapMax, maxDisplayBoost, hdrCapacityMin, hdrCapacityMax, sdr, gainMap }) {
super();
this.name = 'GainMapDecoderMaterial';
this.blending = NoBlending;
this.depthTest = false;
this.depthWrite = false;
this._sdrTexture = texture(sdr);
this._gainMapTexture = texture(gainMap);
// Create uniform nodes
this._gammaUniform = uniform(vec3(1.0 / gamma[0], 1.0 / gamma[1], 1.0 / gamma[2]));
this._offsetHdrUniform = uniform(vec3(offsetHdr[0], offsetHdr[1], offsetHdr[2]));
this._offsetSdrUniform = uniform(vec3(offsetSdr[0], offsetSdr[1], offsetSdr[2]));
this._gainMapMinUniform = uniform(vec3(gainMapMin[0], gainMapMin[1], gainMapMin[2]));
this._gainMapMaxUniform = uniform(vec3(gainMapMax[0], gainMapMax[1], gainMapMax[2]));
const weightFactor = (Math.log2(maxDisplayBoost) - hdrCapacityMin) / (hdrCapacityMax - hdrCapacityMin);
this._weightFactorUniform = uniform(weightFactor);
this._maxDisplayBoost = maxDisplayBoost;
this._hdrCapacityMin = hdrCapacityMin;
this._hdrCapacityMax = hdrCapacityMax;
// Build the TSL shader graph
// Get RGB values
const rgb = this._sdrTexture.rgb;
const recovery = this._gainMapTexture.rgb;
// Apply gamma correction
const logRecovery = pow(recovery, this._gammaUniform);
// Calculate log boost
// logBoost = gainMapMin * (1.0 - logRecovery) + gainMapMax * logRecovery
const oneMinusLogRecovery = sub(float(1.0), logRecovery);
const logBoost = add(mul(this._gainMapMinUniform, oneMinusLogRecovery), mul(this._gainMapMaxUniform, logRecovery));
// Calculate HDR color
// hdrColor = (rgb + offsetSdr) * exp2(logBoost * weightFactor) - offsetHdr
const hdrColor = sub(mul(add(rgb, this._offsetSdrUniform), exp2(mul(logBoost, this._weightFactorUniform))), this._offsetHdrUniform);
// Clamp to half float range
const clampedHdrColor = max(HALF_FLOAT_MIN, min(HALF_FLOAT_MAX, hdrColor));
// Set the color output
this.colorNode = clampedHdrColor;
}
get sdr() { return this._sdrTexture.value; }
set sdr(value) { this._sdrTexture.value = value; }
get gainMap() { return this._gainMapTexture.value; }
set gainMap(value) { this._gainMapTexture.value = value; }
/**
* @see {@link GainMapMetadata.offsetHdr}
*/
get offsetHdr() {
return [this._offsetHdrUniform.value.x, this._offsetHdrUniform.value.y, this._offsetHdrUniform.value.z];
}
set offsetHdr(value) {
this._offsetHdrUniform.value.x = value[0];
this._offsetHdrUniform.value.y = value[1];
this._offsetHdrUniform.value.z = value[2];
}
/**
* @see {@link GainMapMetadata.offsetSdr}
*/
get offsetSdr() {
return [this._offsetSdrUniform.value.x, this._offsetSdrUniform.value.y, this._offsetSdrUniform.value.z];
}
set offsetSdr(value) {
this._offsetSdrUniform.value.x = value[0];
this._offsetSdrUniform.value.y = value[1];
this._offsetSdrUniform.value.z = value[2];
}
/**
* @see {@link GainMapMetadata.gainMapMin}
*/
get gainMapMin() {
return [this._gainMapMinUniform.value.x, this._gainMapMinUniform.value.y, this._gainMapMinUniform.value.z];
}
set gainMapMin(value) {
this._gainMapMinUniform.value.x = value[0];
this._gainMapMinUniform.value.y = value[1];
this._gainMapMinUniform.value.z = value[2];
}
/**
* @see {@link GainMapMetadata.gainMapMax}
*/
get gainMapMax() {
return [this._gainMapMaxUniform.value.x, this._gainMapMaxUniform.value.y, this._gainMapMaxUniform.value.z];
}
set gainMapMax(value) {
this._gainMapMaxUniform.value.x = value[0];
this._gainMapMaxUniform.value.y = value[1];
this._gainMapMaxUniform.value.z = value[2];
}
/**
* @see {@link GainMapMetadata.gamma}
*/
get gamma() {
return [1 / this._gammaUniform.value.x, 1 / this._gammaUniform.value.y, 1 / this._gammaUniform.value.z];
}
set gamma(value) {
this._gammaUniform.value.x = 1.0 / value[0];
this._gammaUniform.value.y = 1.0 / value[1];
this._gammaUniform.value.z = 1.0 / value[2];
}
/**
* @see {@link GainMapMetadata.hdrCapacityMin}
* @remarks Logarithmic space
*/
get hdrCapacityMin() { return this._hdrCapacityMin; }
set hdrCapacityMin(value) {
this._hdrCapacityMin = value;
this.calculateWeight();
}
/**
* @see {@link GainMapMetadata.hdrCapacityMax}
* @remarks Logarithmic space
*/
get hdrCapacityMax() { return this._hdrCapacityMax; }
set hdrCapacityMax(value) {
this._hdrCapacityMax = value;
this.calculateWeight();
}
/**
* @see {@link GainmapDecodingParameters.maxDisplayBoost}
* @remarks Non Logarithmic space
*/
get maxDisplayBoost() { return this._maxDisplayBoost; }
set maxDisplayBoost(value) {
this._maxDisplayBoost = Math.max(1, Math.min(65504, value));
this.calculateWeight();
}
calculateWeight() {
const val = (Math.log2(this._maxDisplayBoost) - this._hdrCapacityMin) / (this._hdrCapacityMax - this._hdrCapacityMin);
this._weightFactorUniform.value = Math.max(0, Math.min(1, val));
}
}
const decodeImpl = createDecodeFunction({
renderer: WebGPURenderer,
createMaterial: (params) => new GainMapDecoderMaterial(params),
createQuadRenderer: (params) => new QuadRenderer(params)
});
/**
* Decodes a gain map using WebGPU RenderTarget
*
* @category Decoding Functions
* @group Decoding Functions
* @example
* import { decode } from '@monogrid/gainmap-js/webgpu'
* import {
* Mesh,
* MeshBasicMaterial,
* PerspectiveCamera,
* PlaneGeometry,
* Scene,
* TextureLoader,
* WebGPURenderer
* } from 'three/webgpu'
*
* const renderer = new WebGPURenderer()
*
* const textureLoader = new TextureLoader()
*
* // load SDR Representation
* const sdr = await textureLoader.loadAsync('sdr.jpg')
* // load Gain map recovery image
* const gainMap = await textureLoader.loadAsync('gainmap.jpg')
* // load metadata
* const metadata = await (await fetch('metadata.json')).json()
*
* const result = await decode({
* sdr,
* gainMap,
* // this allows to use `result.renderTarget.texture` directly
* renderer,
* // this will restore the full HDR range
* maxDisplayBoost: Math.pow(2, metadata.hdrCapacityMax),
* ...metadata
* })
*
* const scene = new Scene()
* // `result` can be used to populate a Texture
* const mesh = new Mesh(
* new PlaneGeometry(),
* new MeshBasicMaterial({ map: result.renderTarget.texture })
* )
* scene.add(mesh)
* renderer.render(scene, new PerspectiveCamera())
*
* // result must be manually disposed
* // when you are done using it
* result.dispose()
*
* @param params
* @returns
* @throws {Error} if the WebGPURenderer fails to render the gain map
*/
const decode = async (params) => {
// Ensure renderer is defined for the base function
if (!params.renderer) {
throw new Error('Renderer is required for decode function');
}
const quadRenderer = decodeImpl({
...params,
renderer: params.renderer
});
try {
await quadRenderer.render();
}
catch (e) {
quadRenderer.disposeOnDemandRenderer();
throw e;
}
return quadRenderer;
};
/**
* Base class for WebGPU loaders
* @template TUrl - The type of URL used to load resources
*/
class LoaderBaseWebGPU extends LoaderBaseShared {
constructor(renderer, manager) {
super({
renderer,
createMaterial: (params) => new GainMapDecoderMaterial(params),
createQuadRenderer: (params) => new QuadRenderer(params)
}, manager);
}
/**
* @private
* @param quadRenderer
* @param metadata
* @param sdrBuffer
* @param gainMapBuffer
*/
async render(quadRenderer, metadata, sdrBuffer, gainMapBuffer) {
// in WebGPU we apparently don't need flipY under any circumstance
// except in QuadRenderer.toDataTexture() where we perform it in the texture itself
const { sdrImage, gainMapImage, needsFlip } = await this.processImages(sdrBuffer, gainMapBuffer, 'from-image');
const { gainMap, sdr } = this.createTextures(sdrImage, gainMapImage, needsFlip);
this.updateQuadRenderer(quadRenderer, sdrImage, gainMap, sdr, metadata);
await quadRenderer.render();
}
}
/**
* A Three.js Loader for the gain map format (WebGPU version).
*
* @category Loaders
* @group Loaders
*
* @example
* import { GainMapLoader } from '@monogrid/gainmap-js/webgpu'
* import {
* EquirectangularReflectionMapping,
* Mesh,
* MeshBasicMaterial,
* PerspectiveCamera,
* PlaneGeometry,
* Scene,
* WebGPURenderer
* } from 'three/webgpu'
*
* const renderer = new WebGPURenderer()
* await renderer.init()
*
* const loader = new GainMapLoader(renderer)
* .setRenderTargetOptions({ mapping: EquirectangularReflectionMapping })
*
* const result = await loader.loadAsync(['sdr.jpeg', 'gainmap.jpeg', 'metadata.json'])
* // `result` can be used to populate a Texture
*
* const scene = new Scene()
* const mesh = new Mesh(
* new PlaneGeometry(),
* new MeshBasicMaterial({ map: result.renderTarget.texture })
* )
* scene.add(mesh)
* renderer.render(scene, new PerspectiveCamera())
*
* // Starting from three.js r159
* // `result.renderTarget.texture` can
* // also be used as Equirectangular scene background
* //
* // it was previously needed to convert it
* // to a DataTexture with `result.toDataTexture()`
* scene.background = result.renderTarget.texture
*
* // result must be manually disposed
* // when you are done using it
* result.dispose()
*
*/
class GainMapLoader extends LoaderBaseWebGPU {
/**
* Loads a gainmap using separate data
* * sdr image
* * gain map image
* * metadata json
*
* useful for webp gain maps
*
* @param urls An array in the form of [sdr.jpg, gainmap.jpg, metadata.json]
* @param onLoad Load complete callback, will receive the result
* @param onProgress Progress callback, will receive a `ProgressEvent`
* @param onError Error callback
* @returns
*/
load([sdrUrl, gainMapUrl, metadataUrl], onLoad, onProgress, onError) {
const quadRenderer = this.prepareQuadRenderer();
let sdr;
let gainMap;
let metadata;
const loadCheck = async () => {
if (sdr && gainMap && metadata) {
// solves #16
try {
await this.render(quadRenderer, metadata, sdr, gainMap);
}
catch (error) {
this.manager.itemError(sdrUrl);
this.manager.itemError(gainMapUrl);
this.manager.itemError(metadataUrl);
if (typeof onError === 'function')
onError(error);
quadRenderer.disposeOnDemandRenderer();
return;
}
if (typeof onLoad === 'function')
onLoad(quadRenderer);
this.manager.itemEnd(sdrUrl);
this.manager.itemEnd(gainMapUrl);
this.manager.itemEnd(metadataUrl);
quadRenderer.disposeOnDemandRenderer();
}
};
let sdrLengthComputable = true;
let sdrTotal = 0;
let sdrLoaded = 0;
let gainMapLengthComputable = true;
let gainMapTotal = 0;
let gainMapLoaded = 0;
let metadataLengthComputable = true;
let metadataTotal = 0;
let metadataLoaded = 0;
const progressHandler = () => {
if (typeof onProgress === 'function') {
const total = sdrTotal + gainMapTotal + metadataTotal;
const loaded = sdrLoaded + gainMapLoaded + metadataLoaded;
const lengthComputable = sdrLengthComputable && gainMapLengthComputable && metadataLengthComputable;
onProgress(new ProgressEvent('progress', { lengthComputable, loaded, total }));
}
};
this.manager.itemStart(sdrUrl);
this.manager.itemStart(gainMapUrl);
this.manager.itemStart(metadataUrl);
const sdrLoader = new FileLoader(this._internalLoadingManager);
sdrLoader.setResponseType('arraybuffer');
sdrLoader.setRequestHeader(this.requestHeader);
sdrLoader.setPath(this.path);
sdrLoader.setWithCredentials(this.withCredentials);
sdrLoader.load(sdrUrl, async (buffer) => {
/* istanbul ignore if
this condition exists only because of three.js types + strict mode
*/
if (typeof buffer === 'string')
throw new Error('Invalid sdr buffer');
sdr = buffer;
await loadCheck();
}, (e) => {
sdrLengthComputable = e.lengthComputable;
sdrLoaded = e.loaded;
sdrTotal = e.total;
progressHandler();
}, (error) => {
this.manager.itemError(sdrUrl);
if (typeof onError === 'function')
onError(error);
});
const gainMapLoader = new FileLoader(this._internalLoadingManager);
gainMapLoader.setResponseType('arraybuffer');
gainMapLoader.setRequestHeader(this.requestHeader);
gainMapLoader.setPath(this.path);
gainMapLoader.setWithCredentials(this.withCredentials);
gainMapLoader.load(gainMapUrl, async (buffer) => {
/* istanbul ignore if
this condition exists only because of three.js types + strict mode
*/
if (typeof buffer === 'string')
throw new Error('Invalid gainmap buffer');
gainMap = buffer;
await loadCheck();
}, (e) => {
gainMapLengthComputable = e.lengthComputable;
gainMapLoaded = e.loaded;
gainMapTotal = e.total;
progressHandler();
}, (error) => {
this.manager.itemError(gainMapUrl);
if (typeof onError === 'function')
onError(error);
});
const metadataLoader = new FileLoader(this._internalLoadingManager);
// metadataLoader.setResponseType('json')
metadataLoader.setRequestHeader(this.requestHeader);
metadataLoader.setPath(this.path);
metadataLoader.setWithCredentials(this.withCredentials);
metadataLoader.load(metadataUrl, async (json) => {
/* istanbul ignore if
this condition exists only because of three.js types + strict mode
*/
if (typeof json !== 'string')
throw new Error('Invalid metadata string');
// TODO: implement check on JSON file and remove this eslint disable
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
metadata = JSON.parse(json);
await loadCheck();
}, (e) => {
metadataLengthComputable = e.lengthComputable;
metadataLoaded = e.loaded;
metadataTotal = e.total;
progressHandler();
}, (error) => {
this.manager.itemError(metadataUrl);
if (typeof onError === 'function')
onError(error);
});
return quadRenderer;
}
}
/**
* A Three.js Loader for a JPEG with embedded gainmap metadata (WebGPU version).
*
* @category Loaders
* @group Loaders
*
* @example
* import { HDRJPGLoader } from '@monogrid/gainmap-js/webgpu'
* import {
* EquirectangularReflectionMapping,
* Mesh,
* MeshBasicMaterial,
* PerspectiveCamera,
* PlaneGeometry,
* Scene,
* WebGPURenderer
* } from 'three/webgpu'
*
* const renderer = new WebGPURenderer()
* await renderer.init()
*
* const loader = new HDRJPGLoader(renderer)
* .setRenderTargetOptions({ mapping: EquirectangularReflectionMapping })
*
* const result = await loader.loadAsync('gainmap.jpeg')
* // `result` can be used to populate a Texture
*
* const scene = new Scene()
* const mesh = new Mesh(
* new PlaneGeometry(),
* new MeshBasicMaterial({ map: result.renderTarget.texture })
* )
* scene.add(mesh)
* renderer.render(scene, new PerspectiveCamera())
*
* // Starting from three.js r159
* // `result.renderTarget.texture` can
* // also be used as Equirectangular scene background
* //
* // it was previously needed to convert it
* // to a DataTexture with `result.toDataTexture()`
* scene.background = result.renderTarget.texture
*
* // result must be manually disposed
* // when you are done using it
* result.dispose()
*
*/
class HDRJPGLoader extends LoaderBaseWebGPU {
/**
* Loads a JPEG containing gain map metadata
* Renders a normal SDR image if gainmap data is not found
*
* @param url Path to a JPEG file containing embedded gain map metadata
* @param onLoad Load complete callback, will receive the result
* @param onProgress Progress callback, will receive a `ProgressEvent`
* @param onError Error callback
* @returns
*/
load(url, onLoad, onProgress, onError) {
const quadRenderer = this.prepareQuadRenderer();
const loader = new FileLoader(this._internalLoadingManager);
loader.setResponseType('arraybuffer');
loader.setRequestHeader(this.requestHeader);
loader.setPath(this.path);
loader.setWithCredentials(this.withCredentials);
this.manager.itemStart(url);
loader.load(url, async (jpeg) => {
/* istanbul ignore if
this condition exists only because of three.js types + strict mode
*/
if (typeof jpeg === 'string')
throw new Error('Invalid buffer, received [string], was expecting [ArrayBuffer]');
const jpegBuffer = new Uint8Array(jpeg);
let sdrJPEG;
let gainMapJPEG;
let metadata;
try {
const extractionResult = await extractGainmapFromJPEG(jpegBuffer);
// gain map is successfully reconstructed
sdrJPEG = extractionResult.sdr;
gainMapJPEG = extractionResult.gainMap;
metadata = extractionResult.metadata;
}
catch (e) {
// render the SDR version if this is not a gainmap
if (e instanceof XMPMetadataNotFoundError || e instanceof GainMapNotFoundError) {
console.warn(`Failure to reconstruct an HDR image from ${url}: Gain map metadata not found in the file, HDRJPGLoader will render the SDR jpeg`);
metadata = {
gainMapMin: [0, 0, 0],
gainMapMax: [1, 1, 1],
gamma: [1, 1, 1],
hdrCapacityMin: 0,
hdrCapacityMax: 1,
offsetHdr: [0, 0, 0],
offsetSdr: [0, 0, 0]
};
sdrJPEG = jpegBuffer;
}
else {
throw e;
}
}
// solves #16
try {
await this.render(quadRenderer, metadata, sdrJPEG.buffer, gainMapJPEG?.buffer);
}
catch (error) {
this.manager.itemError(url);
if (typeof onError === 'function')
onError(error);
quadRenderer.disposeOnDemandRenderer();
return;
}
if (typeof onLoad === 'function')
onLoad(quadRenderer);
this.manager.itemEnd(url);
quadRenderer.disposeOnDemandRenderer();
}, onProgress, (error) => {
this.manager.itemError(url);
if (typeof onError === 'function')
onError(error);
});
return quadRenderer;
}
}
export { GainMapDecoderMaterial, GainMapLoader, GainMapNotFoundError, HDRJPGLoader, HDRJPGLoader as JPEGRLoader, LoaderBaseShared, QuadRenderer, XMPMetadataNotFoundError, createDecodeFunction, decode, extractGainmapFromJPEG };
File diff suppressed because one or more lines are too long
File diff suppressed because it is too large Load Diff
File diff suppressed because one or more lines are too long
+1
View File
@@ -0,0 +1 @@
export * from './webgl/index';
+59
View File
@@ -0,0 +1,59 @@
import { HalfFloatType, LinearSRGBColorSpace, Loader, LoadingManager, Texture } from 'three';
import { type GainMapMetadata, QuadRendererTextureOptions } from '../../core/types';
import { DecodeParameters } from './types';
/**
* Configuration for the loader base class
*/
export interface LoaderBaseConfig<TRenderer, TQuadRenderer, TMaterial> {
renderer?: TRenderer;
renderTargetOptions?: QuadRendererTextureOptions;
createMaterial: (params: DecodeParameters) => TMaterial;
createQuadRenderer: (params: {
width: number;
height: number;
type: typeof HalfFloatType;
colorSpace: typeof LinearSRGBColorSpace;
material: TMaterial;
renderer?: TRenderer;
renderTargetOptions?: QuadRendererTextureOptions;
}) => TQuadRenderer;
}
/**
* Shared base class for loaders that extracts common logic
*/
export declare abstract class LoaderBaseShared<TRenderer, TQuadRenderer, TMaterial, TUrl = string> extends Loader<TQuadRenderer, TUrl> {
private _renderer?;
private _renderTargetOptions?;
protected _internalLoadingManager: LoadingManager;
protected _config: LoaderBaseConfig<TRenderer, TQuadRenderer, TMaterial>;
constructor(config: LoaderBaseConfig<TRenderer, TQuadRenderer, TMaterial>, manager?: LoadingManager);
setRenderer(renderer: TRenderer): this;
setRenderTargetOptions(options: QuadRendererTextureOptions): this;
protected prepareQuadRenderer(): TQuadRenderer;
protected processImages(sdrBuffer: ArrayBuffer, gainMapBuffer?: ArrayBuffer, imageOrientation?: 'flipY' | 'from-image'): Promise<{
sdrImage: ImageBitmap | HTMLImageElement;
gainMapImage: ImageBitmap | HTMLImageElement | undefined;
needsFlip: boolean;
}>;
protected createTextures(sdrImage: ImageBitmap | HTMLImageElement, gainMapImage: ImageBitmap | HTMLImageElement | undefined, needsFlip: boolean): {
gainMap: Texture;
sdr: Texture;
};
protected updateQuadRenderer(quadRenderer: TQuadRenderer & {
width: number;
height: number;
material: TMaterial & {
gainMap: Texture;
sdr: Texture;
gainMapMin: [number, number, number];
gainMapMax: [number, number, number];
offsetHdr: [number, number, number];
offsetSdr: [number, number, number];
gamma: [number, number, number];
hdrCapacityMin: number;
hdrCapacityMax: number;
maxDisplayBoost: number;
needsUpdate: boolean;
};
}, sdrImage: ImageBitmap | HTMLImageElement, gainMap: Texture, sdr: Texture, metadata: GainMapMetadata): void;
}
+24
View File
@@ -0,0 +1,24 @@
import { HalfFloatType, LinearSRGBColorSpace } from 'three';
import { Constructor, QuadRendererTextureOptions } from '../../core';
import { DecodeParametersWithRenderer } from './types';
/**
* Configuration for decode function
*/
export interface DecodeConfig<TRenderer extends Constructor, TQuadRenderer, TMaterial> {
renderer: TRenderer;
createMaterial: (params: DecodeParametersWithRenderer<InstanceType<TRenderer>>) => TMaterial;
createQuadRenderer: (params: {
width: number;
height: number;
type: typeof HalfFloatType;
colorSpace: typeof LinearSRGBColorSpace;
material: TMaterial;
renderer: InstanceType<TRenderer>;
renderTargetOptions?: QuadRendererTextureOptions;
}) => TQuadRenderer;
}
/**
* Shared decode implementation factory
* Creates a decode function that prepares a QuadRenderer with the given parameters
*/
export declare function createDecodeFunction<TRenderer extends Constructor, TQuadRenderer, TMaterial>(config: DecodeConfig<TRenderer, TQuadRenderer, TMaterial>): (params: DecodeParametersWithRenderer<InstanceType<TRenderer>>) => TQuadRenderer;
@@ -0,0 +1,2 @@
export declare class GainMapNotFoundError extends Error {
}
@@ -0,0 +1,2 @@
export declare class XMPMetadataNotFoundError extends Error {
}
+25
View File
@@ -0,0 +1,25 @@
/**
* Extracts XMP Metadata and the gain map recovery image
* from a single JPEG file.
*
* @category Decoding Functions
* @group Decoding Functions
* @param jpegFile an `Uint8Array` containing and encoded JPEG file
* @returns an sdr `Uint8Array` compressed in JPEG, a gainMap `Uint8Array` compressed in JPEG and the XMP parsed XMP metadata
* @throws Error if XMP Metadata is not found
* @throws Error if Gain map image is not found
* @example
* import { FileLoader } from 'three'
* import { extractGainmapFromJPEG } from '@monogrid/gainmap-js'
*
* const jpegFile = await new FileLoader()
* .setResponseType('arraybuffer')
* .loadAsync('image.jpg')
*
* const { sdr, gainMap, metadata } = extractGainmapFromJPEG(jpegFile)
*/
export declare const extractGainmapFromJPEG: (jpegFile: Uint8Array<ArrayBuffer>) => Promise<{
sdr: Uint8Array<ArrayBuffer>;
gainMap: Uint8Array<ArrayBuffer>;
metadata: import("..").GainMapMetadata;
}>;
+8
View File
@@ -0,0 +1,8 @@
export * from './decode';
export * from './errors/GainMapNotFoundError';
export * from './errors/XMPMetadataNotFoundError';
export * from './extract';
export * from './Loader';
export * from './types';
export * from './utils/extractXMP';
export * from './utils/MPFExtractor';
+43
View File
@@ -0,0 +1,43 @@
import { Texture } from 'three';
import { GainMapMetadata, QuadRendererTextureOptions, TextureImageFormat } from '../../core';
/**
* Parameters related to the display used for decoding
*/
export type GainmapDecodingParameters = {
/**
* The maximum available boost supported by a display, at a given point in time.
*
* @remarks
* This value can change over time based on device settings and other factors,
* such as ambient light conditions, or how many bright pixels are on the screen.
*
* Non Logarithmic space
*/
maxDisplayBoost: number;
};
/**
* Parameters for generic decoding
*/
export type DecodeParameters = {
/**
* A Texture containing the SDR Rendition
*/
sdr: Texture<TextureImageFormat>;
/**
* A Texture containing the GainMap recovery image
*/
gainMap: Texture<TextureImageFormat>;
} & GainmapDecodingParameters & GainMapMetadata;
/**
* Parameters for decode function
*/
export type DecodeParametersWithRenderer<TRenderer> = {
/**
* The renderer used to decode the GainMap
*/
renderer: TRenderer;
/**
* Options to use when creating the output renderTarget
*/
renderTargetOptions?: QuadRendererTextureOptions;
} & DecodeParameters & GainmapDecodingParameters & GainMapMetadata;
@@ -0,0 +1,21 @@
export type MPFExtractorOptions = {
debug: boolean;
extractFII: boolean;
extractNonFII: boolean;
};
/**
* MPF Extractor (Multi Picture Format Extractor)
* By Henrik S Nilsson 2019
*
* Extracts images stored in images based on the MPF format (found here: https://www.cipa.jp/e/std/std-sec.html
* under "CIPA DC-007-Translation-2021 Multi-Picture Format"
*
* Overly commented, and without intention of being complete or production ready.
* Created to extract depth maps from iPhone images, and to learn about image metadata.
* Kudos to: Phil Harvey (exiftool), Jaume Sanchez (android-lens-blur-depth-extractor)
*/
export declare class MPFExtractor {
options: MPFExtractorOptions;
constructor(options?: Partial<MPFExtractorOptions>);
extract(imageArrayBuffer: Uint8Array<ArrayBuffer>): Promise<Blob[]>;
}
@@ -0,0 +1,2 @@
import { GainMapMetadata } from '../../../core/types';
export declare const extractXMP: (input: Uint8Array) => GainMapMetadata | undefined;
@@ -0,0 +1,7 @@
/**
* private function, async get image from blob
*
* @param blob
* @returns
*/
export declare const getHTMLImageFromBlob: (blob: Blob) => Promise<HTMLImageElement>;
+60
View File
@@ -0,0 +1,60 @@
import { HalfFloatType, WebGLRenderer } from 'three';
import { QuadRenderer } from '../../core/QuadRenderer';
import { DecodeParametersWithRenderer } from '../shared';
import { GainMapDecoderMaterial } from './materials/GainMapDecoderMaterial';
/**
* Decodes a gain map using a WebGL RenderTarget
*
* @category Decoding Functions
* @group Decoding Functions
* @example
* import { decode } from '@monogrid/gainmap-js'
* import {
* Mesh,
* MeshBasicMaterial,
* PerspectiveCamera,
* PlaneGeometry,
* Scene,
* TextureLoader,
* WebGLRenderer
* } from 'three'
*
* const renderer = new WebGLRenderer()
*
* const textureLoader = new TextureLoader()
*
* // load SDR Representation
* const sdr = await textureLoader.loadAsync('sdr.jpg')
* // load Gain map recovery image
* const gainMap = await textureLoader.loadAsync('gainmap.jpg')
* // load metadata
* const metadata = await (await fetch('metadata.json')).json()
*
* const result = decode({
* sdr,
* gainMap,
* // this allows to use `result.renderTarget.texture` directly
* renderer,
* // this will restore the full HDR range
* maxDisplayBoost: Math.pow(2, metadata.hdrCapacityMax),
* ...metadata
* })
*
* const scene = new Scene()
* // `result` can be used to populate a Texture
* const mesh = new Mesh(
* new PlaneGeometry(),
* new MeshBasicMaterial({ map: result.renderTarget.texture })
* )
* scene.add(mesh)
* renderer.render(scene, new PerspectiveCamera())
*
* // result must be manually disposed
* // when you are done using it
* result.dispose()
*
* @param params
* @returns
* @throws {Error} if the WebGLRenderer fails to render the gain map
*/
export declare const decode: (params: DecodeParametersWithRenderer<WebGLRenderer>) => InstanceType<typeof QuadRenderer<typeof HalfFloatType, InstanceType<typeof GainMapDecoderMaterial>>>;
+8
View File
@@ -0,0 +1,8 @@
export * from '../../core/QuadRenderer';
export * from '../../core/types';
export * from '../shared';
export * from './decode';
export * from './loaders/GainMapLoader';
export * from './loaders/HDRJPGLoader';
export { HDRJPGLoader as JPEGRLoader } from './loaders/HDRJPGLoader';
export * from './materials/GainMapDecoderMaterial';
@@ -0,0 +1,68 @@
import { HalfFloatType } from 'three';
import { QuadRenderer } from '../../../core/QuadRenderer';
import { GainMapDecoderMaterial } from '../materials/GainMapDecoderMaterial';
import { LoaderBaseWebGL } from './LoaderBaseWebGL';
/**
* A Three.js Loader for the gain map format.
*
* @category Loaders
* @group Loaders
*
* @example
* import { GainMapLoader } from '@monogrid/gainmap-js'
* import {
* EquirectangularReflectionMapping,
* Mesh,
* MeshBasicMaterial,
* PerspectiveCamera,
* PlaneGeometry,
* Scene,
* WebGLRenderer
* } from 'three'
*
* const renderer = new WebGLRenderer()
*
* const loader = new GainMapLoader(renderer)
* .setRenderTargetOptions({ mapping: EquirectangularReflectionMapping })
*
* const result = await loader.loadAsync(['sdr.jpeg', 'gainmap.jpeg', 'metadata.json'])
* // `result` can be used to populate a Texture
*
* const scene = new Scene()
* const mesh = new Mesh(
* new PlaneGeometry(),
* new MeshBasicMaterial({ map: result.renderTarget.texture })
* )
* scene.add(mesh)
* renderer.render(scene, new PerspectiveCamera())
*
* // Starting from three.js r159
* // `result.renderTarget.texture` can
* // also be used as Equirectangular scene background
* //
* // it was previously needed to convert it
* // to a DataTexture with `result.toDataTexture()`
* scene.background = result.renderTarget.texture
*
* // result must be manually disposed
* // when you are done using it
* result.dispose()
*
*/
export declare class GainMapLoader extends LoaderBaseWebGL<[string, string, string]> {
/**
* Loads a gainmap using separate data
* * sdr image
* * gain map image
* * metadata json
*
* useful for webp gain maps
*
* @param urls An array in the form of [sdr.jpg, gainmap.jpg, metadata.json]
* @param onLoad Load complete callback, will receive the result
* @param onProgress Progress callback, will receive a `ProgressEvent`
* @param onError Error callback
* @returns
*/
load([sdrUrl, gainMapUrl, metadataUrl]: [string, string, string], onLoad?: (data: QuadRenderer<typeof HalfFloatType, GainMapDecoderMaterial>) => void, onProgress?: (event: ProgressEvent) => void, onError?: (err: unknown) => void): QuadRenderer<typeof HalfFloatType, GainMapDecoderMaterial>;
}
@@ -0,0 +1,64 @@
import { HalfFloatType } from 'three';
import { QuadRenderer } from '../../../core/QuadRenderer';
import { GainMapDecoderMaterial } from '../materials/GainMapDecoderMaterial';
import { LoaderBaseWebGL } from './LoaderBaseWebGL';
/**
* A Three.js Loader for a JPEG with embedded gainmap metadata.
*
* @category Loaders
* @group Loaders
*
* @example
* import { HDRJPGLoader } from '@monogrid/gainmap-js'
* import {
* EquirectangularReflectionMapping,
* Mesh,
* MeshBasicMaterial,
* PerspectiveCamera,
* PlaneGeometry,
* Scene,
* WebGLRenderer
* } from 'three'
*
* const renderer = new WebGLRenderer()
*
* const loader = new HDRJPGLoader(renderer)
* .setRenderTargetOptions({ mapping: EquirectangularReflectionMapping })
*
* const result = await loader.loadAsync('gainmap.jpeg')
* // `result` can be used to populate a Texture
*
* const scene = new Scene()
* const mesh = new Mesh(
* new PlaneGeometry(),
* new MeshBasicMaterial({ map: result.renderTarget.texture })
* )
* scene.add(mesh)
* renderer.render(scene, new PerspectiveCamera())
*
* // Starting from three.js r159
* // `result.renderTarget.texture` can
* // also be used as Equirectangular scene background
* //
* // it was previously needed to convert it
* // to a DataTexture with `result.toDataTexture()`
* scene.background = result.renderTarget.texture
*
* // result must be manually disposed
* // when you are done using it
* result.dispose()
*
*/
export declare class HDRJPGLoader extends LoaderBaseWebGL<string> {
/**
* Loads a JPEG containing gain map metadata
* Renders a normal SDR image if gainmap data is not found
*
* @param url Path to a JPEG file containing embedded gain map metadata
* @param onLoad Load complete callback, will receive the result
* @param onProgress Progress callback, will receive a `ProgressEvent`
* @param onError Error callback
* @returns
*/
load(url: string, onLoad?: (data: QuadRenderer<typeof HalfFloatType, GainMapDecoderMaterial>) => void, onProgress?: (event: ProgressEvent) => void, onError?: (err: unknown) => void): QuadRenderer<typeof HalfFloatType, GainMapDecoderMaterial>;
}
@@ -0,0 +1,20 @@
import { HalfFloatType, LoadingManager, type WebGLRenderer } from 'three';
import { QuadRenderer } from '../../../core/QuadRenderer';
import { type GainMapMetadata } from '../../../core/types';
import { LoaderBaseShared } from '../../shared';
import { GainMapDecoderMaterial } from '../materials/GainMapDecoderMaterial';
/**
* Base class for WebGL loaders
* @template TUrl - The type of URL used to load resources
*/
export declare abstract class LoaderBaseWebGL<TUrl = string> extends LoaderBaseShared<WebGLRenderer, QuadRenderer<typeof HalfFloatType, GainMapDecoderMaterial>, GainMapDecoderMaterial, TUrl> {
constructor(renderer?: WebGLRenderer, manager?: LoadingManager);
/**
* @private
* @param quadRenderer
* @param metadata
* @param sdrBuffer
* @param gainMapBuffer
*/
protected render(quadRenderer: QuadRenderer<typeof HalfFloatType, GainMapDecoderMaterial>, metadata: GainMapMetadata, sdrBuffer: ArrayBuffer, gainMapBuffer?: ArrayBuffer): Promise<void>;
}
@@ -0,0 +1,70 @@
import { ShaderMaterial, Texture } from 'three';
import { GainMapMetadata } from '../../../core/types';
import { type GainmapDecodingParameters } from '../../shared';
/**
* A Material which is able to decode the Gainmap into a full HDR Representation
*
* @category Materials
* @group Materials
*/
export declare class GainMapDecoderMaterial extends ShaderMaterial {
private _maxDisplayBoost;
private _hdrCapacityMin;
private _hdrCapacityMax;
/**
*
* @param params
*/
constructor({ gamma, offsetHdr, offsetSdr, gainMapMin, gainMapMax, maxDisplayBoost, hdrCapacityMin, hdrCapacityMax, sdr, gainMap }: GainMapMetadata & GainmapDecodingParameters & {
sdr: Texture;
gainMap: Texture;
});
get sdr(): Texture;
set sdr(value: Texture);
get gainMap(): Texture;
set gainMap(value: Texture);
/**
* @see {@link GainMapMetadata.offsetHdr}
*/
get offsetHdr(): [number, number, number];
set offsetHdr(value: [number, number, number]);
/**
* @see {@link GainMapMetadata.offsetSdr}
*/
get offsetSdr(): [number, number, number];
set offsetSdr(value: [number, number, number]);
/**
* @see {@link GainMapMetadata.gainMapMin}
*/
get gainMapMin(): [number, number, number];
set gainMapMin(value: [number, number, number]);
/**
* @see {@link GainMapMetadata.gainMapMax}
*/
get gainMapMax(): [number, number, number];
set gainMapMax(value: [number, number, number]);
/**
* @see {@link GainMapMetadata.gamma}
*/
get gamma(): [number, number, number];
set gamma(value: [number, number, number]);
/**
* @see {@link GainMapMetadata.hdrCapacityMin}
* @remarks Logarithmic space
*/
get hdrCapacityMin(): number;
set hdrCapacityMin(value: number);
/**
* @see {@link GainMapMetadata.hdrCapacityMin}
* @remarks Logarithmic space
*/
get hdrCapacityMax(): number;
set hdrCapacityMax(value: number);
/**
* @see {@link GainmapDecodingParameters.maxDisplayBoost}
* @remarks Non Logarithmic space
*/
get maxDisplayBoost(): number;
set maxDisplayBoost(value: number);
private calculateWeight;
}
@@ -0,0 +1,153 @@
import { ByteType, ColorSpace, DataTexture, FloatType, HalfFloatType, IntType, Material, RenderTarget, ShortType, TextureDataType, UnsignedByteType, UnsignedInt248Type, UnsignedInt5999Type, UnsignedInt101111Type, UnsignedIntType, UnsignedShort4444Type, UnsignedShort5551Type, UnsignedShortType, WebGPURenderer } from 'three/webgpu';
import { QuadRendererTextureOptions } from '../../../core/types';
/**
* Utility Type that translates `three` texture types to their TypedArray counterparts.
*
* @category Utility
* @group Utility
*/
export type TextureDataTypeToBufferType<TType extends TextureDataType> = TType extends typeof UnsignedByteType ? Uint8ClampedArray<ArrayBuffer> : TType extends typeof ByteType ? Int8Array<ArrayBuffer> : TType extends typeof ShortType ? Int16Array<ArrayBuffer> : TType extends typeof UnsignedShortType ? Uint16Array<ArrayBuffer> : TType extends typeof IntType ? Int32Array<ArrayBuffer> : TType extends typeof UnsignedIntType ? Uint32Array<ArrayBuffer> : TType extends typeof FloatType ? Float32Array<ArrayBuffer> : TType extends typeof HalfFloatType ? Uint16Array<ArrayBuffer> : TType extends typeof UnsignedShort4444Type ? Uint16Array<ArrayBuffer> : TType extends typeof UnsignedShort5551Type ? Uint16Array<ArrayBuffer> : TType extends typeof UnsignedInt248Type ? Uint32Array<ArrayBuffer> : TType extends typeof UnsignedInt5999Type ? Uint32Array<ArrayBuffer> : TType extends typeof UnsignedInt101111Type ? Uint32Array<ArrayBuffer> : never;
export type QuadRendererOptions<TType extends TextureDataType, TMaterial extends Material> = {
/**
* Width of the render target
*/
width: number;
/**
* height of the renderTarget
*/
height: number;
/**
* TextureDataType of the renderTarget
*/
type: TType;
/**
* ColorSpace of the renderTarget
*/
colorSpace: ColorSpace;
/**
* material to use for rendering
*/
material: TMaterial;
/**
* Renderer instance to use
*/
renderer?: WebGPURenderer;
/**
* Additional renderTarget options
*/
renderTargetOptions?: QuadRendererTextureOptions;
};
/**
* Utility class used for rendering a texture with a material (WebGPU version)
*
* @category Core
* @group Core
*/
export declare class QuadRenderer<TType extends TextureDataType, TMaterial extends Material> {
private _renderer;
private _rendererIsDisposable;
private _material;
private _scene;
private _camera;
private _quad;
private _renderTarget;
private _width;
private _height;
private _type;
private _colorSpace;
private _supportsReadPixels;
/**
* Constructs a new QuadRenderer
*
* @param options Parameters for this QuadRenderer
*/
constructor(options: QuadRendererOptions<TType, TMaterial>);
/**
* Instantiates a temporary renderer
*
* @returns
*/
static instantiateRenderer(): WebGPURenderer;
/**
* Renders the input texture using the specified material
*/
render: () => Promise<void>;
/**
* Obtains a Buffer containing the rendered texture.
*
* @throws Error if the browser cannot read pixels from this RenderTarget type.
* @returns a TypedArray containing RGBA values from this renderer
*/
toArray(): Promise<TextureDataTypeToBufferType<TType>>;
/**
* Performs a readPixel operation in the renderTarget
* and returns a DataTexture containing the read data
*
* @param options options
* @returns
*/
toDataTexture(options?: QuadRendererTextureOptions): Promise<DataTexture>;
/**
* If using a disposable renderer, it will dispose it.
*/
disposeOnDemandRenderer(): void;
/**
* Will dispose of **all** assets used by this renderer.
*
*
* @param disposeRenderTarget will dispose of the renderTarget which will not be usable later
* set this to true if you passed the `renderTarget.texture` to a `PMREMGenerator`
* or are otherwise done with it.
*
* @example
* ```js
* const loader = new HDRJPGLoader(renderer)
* const result = await loader.loadAsync('gainmap.jpeg')
* const mesh = new Mesh(geometry, new MeshBasicMaterial({ map: result.renderTarget.texture }) )
* // DO NOT dispose the renderTarget here,
* // it is used directly in the material
* result.dispose()
* ```
*
* @example
* ```js
* const loader = new HDRJPGLoader(renderer)
* const pmremGenerator = new PMREMGenerator( renderer );
* const result = await loader.loadAsync('gainmap.jpeg')
* const envMap = pmremGenerator.fromEquirectangular(result.renderTarget.texture)
* const mesh = new Mesh(geometry, new MeshStandardMaterial({ envMap }) )
* // renderTarget can be disposed here
* // because it was used to generate a PMREM texture
* result.dispose(true)
* ```
*/
dispose(disposeRenderTarget?: boolean): void;
/**
* Width of the texture
*/
get width(): number;
set width(value: number);
/**
* Height of the texture
*/
get height(): number;
set height(value: number);
/**
* The renderer used
*/
get renderer(): WebGPURenderer;
/**
* The `RenderTarget` used.
*/
get renderTarget(): RenderTarget;
set renderTarget(value: RenderTarget);
/**
* The `Material` used.
*/
get material(): TMaterial;
/**
*
*/
get type(): TType;
get colorSpace(): ColorSpace;
}
+60
View File
@@ -0,0 +1,60 @@
import { HalfFloatType, WebGPURenderer } from 'three/webgpu';
import { DecodeParametersWithRenderer } from '../shared';
import { QuadRenderer } from './core/QuadRenderer';
import { GainMapDecoderMaterial } from './materials/GainMapDecoderMaterial';
/**
* Decodes a gain map using WebGPU RenderTarget
*
* @category Decoding Functions
* @group Decoding Functions
* @example
* import { decode } from '@monogrid/gainmap-js/webgpu'
* import {
* Mesh,
* MeshBasicMaterial,
* PerspectiveCamera,
* PlaneGeometry,
* Scene,
* TextureLoader,
* WebGPURenderer
* } from 'three/webgpu'
*
* const renderer = new WebGPURenderer()
*
* const textureLoader = new TextureLoader()
*
* // load SDR Representation
* const sdr = await textureLoader.loadAsync('sdr.jpg')
* // load Gain map recovery image
* const gainMap = await textureLoader.loadAsync('gainmap.jpg')
* // load metadata
* const metadata = await (await fetch('metadata.json')).json()
*
* const result = await decode({
* sdr,
* gainMap,
* // this allows to use `result.renderTarget.texture` directly
* renderer,
* // this will restore the full HDR range
* maxDisplayBoost: Math.pow(2, metadata.hdrCapacityMax),
* ...metadata
* })
*
* const scene = new Scene()
* // `result` can be used to populate a Texture
* const mesh = new Mesh(
* new PlaneGeometry(),
* new MeshBasicMaterial({ map: result.renderTarget.texture })
* )
* scene.add(mesh)
* renderer.render(scene, new PerspectiveCamera())
*
* // result must be manually disposed
* // when you are done using it
* result.dispose()
*
* @param params
* @returns
* @throws {Error} if the WebGPURenderer fails to render the gain map
*/
export declare const decode: (params: DecodeParametersWithRenderer<WebGPURenderer>) => Promise<InstanceType<typeof QuadRenderer<typeof HalfFloatType, InstanceType<typeof GainMapDecoderMaterial>>>>;
+8
View File
@@ -0,0 +1,8 @@
export * from '../../core/types';
export * from '../shared';
export * from './core/QuadRenderer';
export * from './decode';
export * from './loaders/GainMapLoader';
export * from './loaders/HDRJPGLoader';
export { HDRJPGLoader as JPEGRLoader } from './loaders/HDRJPGLoader';
export * from './materials/GainMapDecoderMaterial';
@@ -0,0 +1,69 @@
import { HalfFloatType } from 'three/webgpu';
import { QuadRenderer } from '../core/QuadRenderer';
import { GainMapDecoderMaterial } from '../materials/GainMapDecoderMaterial';
import { LoaderBaseWebGPU } from './LoaderBaseWebGPU';
/**
* A Three.js Loader for the gain map format (WebGPU version).
*
* @category Loaders
* @group Loaders
*
* @example
* import { GainMapLoader } from '@monogrid/gainmap-js/webgpu'
* import {
* EquirectangularReflectionMapping,
* Mesh,
* MeshBasicMaterial,
* PerspectiveCamera,
* PlaneGeometry,
* Scene,
* WebGPURenderer
* } from 'three/webgpu'
*
* const renderer = new WebGPURenderer()
* await renderer.init()
*
* const loader = new GainMapLoader(renderer)
* .setRenderTargetOptions({ mapping: EquirectangularReflectionMapping })
*
* const result = await loader.loadAsync(['sdr.jpeg', 'gainmap.jpeg', 'metadata.json'])
* // `result` can be used to populate a Texture
*
* const scene = new Scene()
* const mesh = new Mesh(
* new PlaneGeometry(),
* new MeshBasicMaterial({ map: result.renderTarget.texture })
* )
* scene.add(mesh)
* renderer.render(scene, new PerspectiveCamera())
*
* // Starting from three.js r159
* // `result.renderTarget.texture` can
* // also be used as Equirectangular scene background
* //
* // it was previously needed to convert it
* // to a DataTexture with `result.toDataTexture()`
* scene.background = result.renderTarget.texture
*
* // result must be manually disposed
* // when you are done using it
* result.dispose()
*
*/
export declare class GainMapLoader extends LoaderBaseWebGPU<[string, string, string]> {
/**
* Loads a gainmap using separate data
* * sdr image
* * gain map image
* * metadata json
*
* useful for webp gain maps
*
* @param urls An array in the form of [sdr.jpg, gainmap.jpg, metadata.json]
* @param onLoad Load complete callback, will receive the result
* @param onProgress Progress callback, will receive a `ProgressEvent`
* @param onError Error callback
* @returns
*/
load([sdrUrl, gainMapUrl, metadataUrl]: [string, string, string], onLoad?: (data: QuadRenderer<typeof HalfFloatType, GainMapDecoderMaterial>) => void, onProgress?: (event: ProgressEvent) => void, onError?: (err: unknown) => void): QuadRenderer<typeof HalfFloatType, GainMapDecoderMaterial>;
}
@@ -0,0 +1,65 @@
import { HalfFloatType } from 'three/webgpu';
import { QuadRenderer } from '../core/QuadRenderer';
import { GainMapDecoderMaterial } from '../materials/GainMapDecoderMaterial';
import { LoaderBaseWebGPU } from './LoaderBaseWebGPU';
/**
* A Three.js Loader for a JPEG with embedded gainmap metadata (WebGPU version).
*
* @category Loaders
* @group Loaders
*
* @example
* import { HDRJPGLoader } from '@monogrid/gainmap-js/webgpu'
* import {
* EquirectangularReflectionMapping,
* Mesh,
* MeshBasicMaterial,
* PerspectiveCamera,
* PlaneGeometry,
* Scene,
* WebGPURenderer
* } from 'three/webgpu'
*
* const renderer = new WebGPURenderer()
* await renderer.init()
*
* const loader = new HDRJPGLoader(renderer)
* .setRenderTargetOptions({ mapping: EquirectangularReflectionMapping })
*
* const result = await loader.loadAsync('gainmap.jpeg')
* // `result` can be used to populate a Texture
*
* const scene = new Scene()
* const mesh = new Mesh(
* new PlaneGeometry(),
* new MeshBasicMaterial({ map: result.renderTarget.texture })
* )
* scene.add(mesh)
* renderer.render(scene, new PerspectiveCamera())
*
* // Starting from three.js r159
* // `result.renderTarget.texture` can
* // also be used as Equirectangular scene background
* //
* // it was previously needed to convert it
* // to a DataTexture with `result.toDataTexture()`
* scene.background = result.renderTarget.texture
*
* // result must be manually disposed
* // when you are done using it
* result.dispose()
*
*/
export declare class HDRJPGLoader extends LoaderBaseWebGPU<string> {
/**
* Loads a JPEG containing gain map metadata
* Renders a normal SDR image if gainmap data is not found
*
* @param url Path to a JPEG file containing embedded gain map metadata
* @param onLoad Load complete callback, will receive the result
* @param onProgress Progress callback, will receive a `ProgressEvent`
* @param onError Error callback
* @returns
*/
load(url: string, onLoad?: (data: QuadRenderer<typeof HalfFloatType, GainMapDecoderMaterial>) => void, onProgress?: (event: ProgressEvent) => void, onError?: (err: unknown) => void): QuadRenderer<typeof HalfFloatType, GainMapDecoderMaterial>;
}
@@ -0,0 +1,20 @@
import { HalfFloatType, LoadingManager, WebGPURenderer } from 'three/webgpu';
import { type GainMapMetadata } from '../../../core/types';
import { LoaderBaseShared } from '../../shared';
import { QuadRenderer } from '../core/QuadRenderer';
import { GainMapDecoderMaterial } from '../materials/GainMapDecoderMaterial';
/**
* Base class for WebGPU loaders
* @template TUrl - The type of URL used to load resources
*/
export declare abstract class LoaderBaseWebGPU<TUrl = string> extends LoaderBaseShared<WebGPURenderer, QuadRenderer<typeof HalfFloatType, GainMapDecoderMaterial>, GainMapDecoderMaterial, TUrl> {
constructor(renderer?: WebGPURenderer, manager?: LoadingManager);
/**
* @private
* @param quadRenderer
* @param metadata
* @param sdrBuffer
* @param gainMapBuffer
*/
protected render(quadRenderer: QuadRenderer<typeof HalfFloatType, GainMapDecoderMaterial>, metadata: GainMapMetadata, sdrBuffer: ArrayBuffer, gainMapBuffer?: ArrayBuffer): Promise<void>;
}
@@ -0,0 +1,78 @@
import { MeshBasicNodeMaterial, Texture } from 'three/webgpu';
import { GainMapMetadata } from '../../../core/types';
import { type GainmapDecodingParameters } from '../../shared';
/**
* A Material which is able to decode the Gainmap into a full HDR Representation using TSL (Three.js Shading Language)
*
* @category Materials
* @group Materials
*/
export declare class GainMapDecoderMaterial extends MeshBasicNodeMaterial {
private _maxDisplayBoost;
private _hdrCapacityMin;
private _hdrCapacityMax;
private _gammaUniform;
private _offsetHdrUniform;
private _offsetSdrUniform;
private _gainMapMinUniform;
private _gainMapMaxUniform;
private _weightFactorUniform;
private _sdrTexture;
private _gainMapTexture;
/**
*
* @param params
*/
constructor({ gamma, offsetHdr, offsetSdr, gainMapMin, gainMapMax, maxDisplayBoost, hdrCapacityMin, hdrCapacityMax, sdr, gainMap }: GainMapMetadata & GainmapDecodingParameters & {
sdr: Texture;
gainMap: Texture;
});
get sdr(): Texture;
set sdr(value: Texture);
get gainMap(): Texture;
set gainMap(value: Texture);
/**
* @see {@link GainMapMetadata.offsetHdr}
*/
get offsetHdr(): [number, number, number];
set offsetHdr(value: [number, number, number]);
/**
* @see {@link GainMapMetadata.offsetSdr}
*/
get offsetSdr(): [number, number, number];
set offsetSdr(value: [number, number, number]);
/**
* @see {@link GainMapMetadata.gainMapMin}
*/
get gainMapMin(): [number, number, number];
set gainMapMin(value: [number, number, number]);
/**
* @see {@link GainMapMetadata.gainMapMax}
*/
get gainMapMax(): [number, number, number];
set gainMapMax(value: [number, number, number]);
/**
* @see {@link GainMapMetadata.gamma}
*/
get gamma(): [number, number, number];
set gamma(value: [number, number, number]);
/**
* @see {@link GainMapMetadata.hdrCapacityMin}
* @remarks Logarithmic space
*/
get hdrCapacityMin(): number;
set hdrCapacityMin(value: number);
/**
* @see {@link GainMapMetadata.hdrCapacityMax}
* @remarks Logarithmic space
*/
get hdrCapacityMax(): number;
set hdrCapacityMax(value: number);
/**
* @see {@link GainmapDecodingParameters.maxDisplayBoost}
* @remarks Non Logarithmic space
*/
get maxDisplayBoost(): number;
set maxDisplayBoost(value: number);
private calculateWeight;
}
+1
View File
@@ -0,0 +1 @@
export * from './encode/index';
+917
View File
@@ -0,0 +1,917 @@
/**
* @monogrid/gainmap-js v3.4.0
* With ❤️, by MONOGRID <gainmap@monogrid.com>
*/
import { c as compress } from './compress-C-X439Jm.js';
import { DataTexture, RGBAFormat, UVMapping, RepeatWrapping, LinearFilter, LinearSRGBColorSpace, ShaderMaterial, NoBlending, Vector3, UnsignedByteType, ACESFilmicToneMapping, LinearToneMapping, NeutralToneMapping, AgXToneMapping, CineonToneMapping, ReinhardToneMapping, SRGBColorSpace, AlphaFormat, RGBFormat, DepthFormat, DepthStencilFormat, RedFormat, RedIntegerFormat, RGFormat, RGIntegerFormat, RGBIntegerFormat, RGBAIntegerFormat, Vector2, WebGLRenderTarget, ClampToEdgeWrapping, NearestFilter, DataUtils, FloatType } from 'three';
import { Q as QuadRenderer } from './QuadRenderer-Bj1xl_EK.js';
/**
* Utility function to obtain a `DataTexture` from various input formats
*
* @category Utility
* @group Utility
*
* @param image
* @returns
*/
const getDataTexture = (image) => {
let dataTexture;
if (image instanceof DataTexture) {
if (!(image.image.data instanceof Uint16Array) && !(image.image.data instanceof Float32Array)) {
throw new Error('Provided image is not HDR');
}
dataTexture = image;
}
else {
dataTexture = new DataTexture(image.data, image.width, image.height, 'format' in image ? image.format : RGBAFormat, image.type, UVMapping, RepeatWrapping, RepeatWrapping, LinearFilter, LinearFilter, 1, 'colorSpace' in image && image.colorSpace === 'srgb' ? image.colorSpace : LinearSRGBColorSpace);
// TODO: This tries to detect a raw RGBE and applies flipY
// see if there's a better way to detect it?
if ('header' in image && 'gamma' in image) {
dataTexture.flipY = true;
}
dataTexture.needsUpdate = true;
}
return dataTexture;
};
const vertexShader$2 = /* glsl */ `
varying vec2 vUv;
void main() {
vUv = uv;
gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
}
`;
const fragmentShader$2 = /* glsl */ `
#ifndef saturate
#define saturate( a ) clamp( a, 0.0, 1.0 )
#endif
uniform sampler2D sdr;
uniform sampler2D hdr;
uniform vec3 gamma;
uniform vec3 offsetSdr;
uniform vec3 offsetHdr;
uniform float minLog2;
uniform float maxLog2;
varying vec2 vUv;
void main() {
vec3 sdrColor = texture2D(sdr, vUv).rgb;
vec3 hdrColor = texture2D(hdr, vUv).rgb;
vec3 pixelGain = (hdrColor + offsetHdr) / (sdrColor + offsetSdr);
vec3 logRecovery = (log2(pixelGain) - minLog2) / (maxLog2 - minLog2);
vec3 clampedRecovery = saturate(logRecovery);
gl_FragColor = vec4(pow(clampedRecovery, gamma), 1.0);
}
`;
/**
* A Material which is able to encode a gainmap
*
* @category Materials
* @group Materials
*/
class GainMapEncoderMaterial extends ShaderMaterial {
_minContentBoost;
_maxContentBoost;
_offsetSdr;
_offsetHdr;
_gamma;
/**
*
* @param params
*/
constructor({ sdr, hdr, offsetSdr, offsetHdr, maxContentBoost, minContentBoost, gamma }) {
if (!maxContentBoost)
throw new Error('maxContentBoost is required');
if (!sdr)
throw new Error('sdr is required');
if (!hdr)
throw new Error('hdr is required');
const _gamma = gamma || [1, 1, 1];
const _offsetSdr = offsetSdr || [1 / 64, 1 / 64, 1 / 64];
const _offsetHdr = offsetHdr || [1 / 64, 1 / 64, 1 / 64];
const _minContentBoost = minContentBoost || 1;
const _maxContentBoost = Math.max(maxContentBoost, 1.0001);
super({
name: 'GainMapEncoderMaterial',
vertexShader: vertexShader$2,
fragmentShader: fragmentShader$2,
uniforms: {
sdr: { value: sdr },
hdr: { value: hdr },
gamma: { value: new Vector3().fromArray(_gamma) },
offsetSdr: { value: new Vector3().fromArray(_offsetSdr) },
offsetHdr: { value: new Vector3().fromArray(_offsetHdr) },
minLog2: { value: Math.log2(_minContentBoost) },
maxLog2: { value: Math.log2(_maxContentBoost) }
},
blending: NoBlending,
depthTest: false,
depthWrite: false
});
this._minContentBoost = _minContentBoost;
this._maxContentBoost = _maxContentBoost;
this._offsetSdr = _offsetSdr;
this._offsetHdr = _offsetHdr;
this._gamma = _gamma;
this.needsUpdate = true;
this.uniformsNeedUpdate = true;
}
/**
* @see {@link GainmapEncodingParameters.gamma}
*/
get gamma() { return this._gamma; }
set gamma(value) {
this._gamma = value;
this.uniforms.gamma.value = new Vector3().fromArray(value);
}
/**
* @see {@link GainmapEncodingParameters.offsetHdr}
*/
get offsetHdr() { return this._offsetHdr; }
set offsetHdr(value) {
this._offsetHdr = value;
this.uniforms.offsetHdr.value = new Vector3().fromArray(value);
}
/**
* @see {@link GainmapEncodingParameters.offsetSdr}
*/
get offsetSdr() { return this._offsetSdr; }
set offsetSdr(value) {
this._offsetSdr = value;
this.uniforms.offsetSdr.value = new Vector3().fromArray(value);
}
/**
* @see {@link GainmapEncodingParameters.minContentBoost}
* @remarks Non logarithmic space
*/
get minContentBoost() { return this._minContentBoost; }
set minContentBoost(value) {
this._minContentBoost = value;
this.uniforms.minLog2.value = Math.log2(value);
}
/**
* @see {@link GainmapEncodingParameters.maxContentBoost}
* @remarks Non logarithmic space
*/
get maxContentBoost() { return this._maxContentBoost; }
set maxContentBoost(value) {
this._maxContentBoost = value;
this.uniforms.maxLog2.value = Math.log2(value);
}
/**
* @see {@link GainMapMetadata.gainMapMin}
* @remarks Logarithmic space
*/
get gainMapMin() { return [Math.log2(this._minContentBoost), Math.log2(this._minContentBoost), Math.log2(this._minContentBoost)]; }
/**
* @see {@link GainMapMetadata.gainMapMax}
* @remarks Logarithmic space
*/
get gainMapMax() { return [Math.log2(this._maxContentBoost), Math.log2(this._maxContentBoost), Math.log2(this._maxContentBoost)]; }
/**
* @see {@link GainMapMetadata.hdrCapacityMin}
* @remarks Logarithmic space
*/
get hdrCapacityMin() { return Math.min(Math.max(0, this.gainMapMin[0]), Math.max(0, this.gainMapMin[1]), Math.max(0, this.gainMapMin[2])); }
/**
* @see {@link GainMapMetadata.hdrCapacityMax}
* @remarks Logarithmic space
*/
get hdrCapacityMax() { return Math.max(Math.max(0, this.gainMapMax[0]), Math.max(0, this.gainMapMax[1]), Math.max(0, this.gainMapMax[2])); }
}
/**
*
* @param params
* @returns
* @category Encoding Functions
* @group Encoding Functions
*/
const getGainMap = (params) => {
const { image, sdr, renderer } = params;
const dataTexture = getDataTexture(image);
const material = new GainMapEncoderMaterial({
...params,
sdr: sdr.renderTarget.texture,
hdr: dataTexture
});
const quadRenderer = new QuadRenderer({
width: dataTexture.image.width,
height: dataTexture.image.height,
type: UnsignedByteType,
colorSpace: LinearSRGBColorSpace,
material,
renderer,
renderTargetOptions: params.renderTargetOptions
});
try {
quadRenderer.render();
}
catch (e) {
quadRenderer.disposeOnDemandRenderer();
throw e;
}
return quadRenderer;
};
const vertexShader$1 = /* glsl */ `
varying vec2 vUv;
void main() {
vUv = uv;
gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
}
`;
const fragmentShader$1 = /* glsl */ `
#ifndef saturate
#define saturate( a ) clamp( a, 0.0, 1.0 )
#endif
uniform sampler2D map;
uniform float brightness;
uniform float contrast;
uniform float saturation;
uniform float exposure;
varying vec2 vUv;
mat4 brightnessMatrix( float brightness ) {
return mat4(
1, 0, 0, 0,
0, 1, 0, 0,
0, 0, 1, 0,
brightness, brightness, brightness, 1
);
}
mat4 contrastMatrix( float contrast ) {
float t = ( 1.0 - contrast ) / 2.0;
return mat4(
contrast, 0, 0, 0,
0, contrast, 0, 0,
0, 0, contrast, 0,
t, t, t, 1
);
}
mat4 saturationMatrix( float saturation ) {
vec3 luminance = vec3( 0.3086, 0.6094, 0.0820 );
float oneMinusSat = 1.0 - saturation;
vec3 red = vec3( luminance.x * oneMinusSat );
red+= vec3( saturation, 0, 0 );
vec3 green = vec3( luminance.y * oneMinusSat );
green += vec3( 0, saturation, 0 );
vec3 blue = vec3( luminance.z * oneMinusSat );
blue += vec3( 0, 0, saturation );
return mat4(
red, 0,
green, 0,
blue, 0,
0, 0, 0, 1
);
}
vec3 RRTAndODTFit( vec3 v ) {
vec3 a = v * ( v + 0.0245786 ) - 0.000090537;
vec3 b = v * ( 0.983729 * v + 0.4329510 ) + 0.238081;
return a / b;
}
vec3 ACESFilmicToneMapping( vec3 color ) {
// sRGB => XYZ => D65_2_D60 => AP1 => RRT_SAT
const mat3 ACESInputMat = mat3(
vec3( 0.59719, 0.07600, 0.02840 ), // transposed from source
vec3( 0.35458, 0.90834, 0.13383 ),
vec3( 0.04823, 0.01566, 0.83777 )
);
// ODT_SAT => XYZ => D60_2_D65 => sRGB
const mat3 ACESOutputMat = mat3(
vec3( 1.60475, -0.10208, -0.00327 ), // transposed from source
vec3( -0.53108, 1.10813, -0.07276 ),
vec3( -0.07367, -0.00605, 1.07602 )
);
color = ACESInputMat * color;
// Apply RRT and ODT
color = RRTAndODTFit( color );
color = ACESOutputMat * color;
// Clamp to [0, 1]
return saturate( color );
}
// source: https://www.cs.utah.edu/docs/techreports/2002/pdf/UUCS-02-001.pdf
vec3 ReinhardToneMapping( vec3 color ) {
return saturate( color / ( vec3( 1.0 ) + color ) );
}
// source: http://filmicworlds.com/blog/filmic-tonemapping-operators/
vec3 CineonToneMapping( vec3 color ) {
// optimized filmic operator by Jim Hejl and Richard Burgess-Dawson
color = max( vec3( 0.0 ), color - 0.004 );
return pow( ( color * ( 6.2 * color + 0.5 ) ) / ( color * ( 6.2 * color + 1.7 ) + 0.06 ), vec3( 2.2 ) );
}
// nothing
vec3 LinearToneMapping ( vec3 color ) {
return color;
}
// Matrices for rec 2020 <> rec 709 color space conversion
// matrix provided in row-major order so it has been transposed
// https://www.itu.int/pub/R-REP-BT.2407-2017
const mat3 LINEAR_REC2020_TO_LINEAR_SRGB = mat3(
vec3( 1.6605, - 0.1246, - 0.0182 ),
vec3( - 0.5876, 1.1329, - 0.1006 ),
vec3( - 0.0728, - 0.0083, 1.1187 )
);
const mat3 LINEAR_SRGB_TO_LINEAR_REC2020 = mat3(
vec3( 0.6274, 0.0691, 0.0164 ),
vec3( 0.3293, 0.9195, 0.0880 ),
vec3( 0.0433, 0.0113, 0.8956 )
);
// https://iolite-engine.com/blog_posts/minimal_agx_implementation
// Mean error^2: 3.6705141e-06
vec3 agxDefaultContrastApprox( vec3 x ) {
vec3 x2 = x * x;
vec3 x4 = x2 * x2;
return + 15.5 * x4 * x2
- 40.14 * x4 * x
+ 31.96 * x4
- 6.868 * x2 * x
+ 0.4298 * x2
+ 0.1191 * x
- 0.00232;
}
// AgX Tone Mapping implementation based on Filament, which in turn is based
// on Blender's implementation using rec 2020 primaries
// https://github.com/google/filament/pull/7236
// Inputs and outputs are encoded as Linear-sRGB.
vec3 AgXToneMapping( vec3 color ) {
// AgX constants
const mat3 AgXInsetMatrix = mat3(
vec3( 0.856627153315983, 0.137318972929847, 0.11189821299995 ),
vec3( 0.0951212405381588, 0.761241990602591, 0.0767994186031903 ),
vec3( 0.0482516061458583, 0.101439036467562, 0.811302368396859 )
);
// explicit AgXOutsetMatrix generated from Filaments AgXOutsetMatrixInv
const mat3 AgXOutsetMatrix = mat3(
vec3( 1.1271005818144368, - 0.1413297634984383, - 0.14132976349843826 ),
vec3( - 0.11060664309660323, 1.157823702216272, - 0.11060664309660294 ),
vec3( - 0.016493938717834573, - 0.016493938717834257, 1.2519364065950405 )
);
// LOG2_MIN = -10.0
// LOG2_MAX = +6.5
// MIDDLE_GRAY = 0.18
const float AgxMinEv = - 12.47393; // log2( pow( 2, LOG2_MIN ) * MIDDLE_GRAY )
const float AgxMaxEv = 4.026069; // log2( pow( 2, LOG2_MAX ) * MIDDLE_GRAY )
color = LINEAR_SRGB_TO_LINEAR_REC2020 * color;
color = AgXInsetMatrix * color;
// Log2 encoding
color = max( color, 1e-10 ); // avoid 0 or negative numbers for log2
color = log2( color );
color = ( color - AgxMinEv ) / ( AgxMaxEv - AgxMinEv );
color = clamp( color, 0.0, 1.0 );
// Apply sigmoid
color = agxDefaultContrastApprox( color );
// Apply AgX look
// v = agxLook(v, look);
color = AgXOutsetMatrix * color;
// Linearize
color = pow( max( vec3( 0.0 ), color ), vec3( 2.2 ) );
color = LINEAR_REC2020_TO_LINEAR_SRGB * color;
// Gamut mapping. Simple clamp for now.
color = clamp( color, 0.0, 1.0 );
return color;
}
// https://modelviewer.dev/examples/tone-mapping
vec3 NeutralToneMapping( vec3 color ) {
const float StartCompression = 0.8 - 0.04;
const float Desaturation = 0.15;
float x = min( color.r, min( color.g, color.b ) );
float offset = x < 0.08 ? x - 6.25 * x * x : 0.04;
color -= offset;
float peak = max( color.r, max( color.g, color.b ) );
if ( peak < StartCompression ) return color;
float d = 1. - StartCompression;
float newPeak = 1. - d * d / ( peak + d - StartCompression );
color *= newPeak / peak;
float g = 1. - 1. / ( Desaturation * ( peak - newPeak ) + 1. );
return mix( color, vec3( newPeak ), g );
}
void main() {
vec4 color = texture2D(map, vUv);
vec4 exposed = vec4(exposure * color.rgb, color.a);
vec4 tonemapped = vec4(TONEMAPPING_FUNCTION(exposed.rgb), color.a);
vec4 adjusted =
brightnessMatrix( brightness ) *
contrastMatrix( contrast ) *
saturationMatrix( saturation ) *
tonemapped;
gl_FragColor = adjusted;
}
`;
/**
* A Material used to adjust the SDR representation of an HDR image
*
* @category Materials
* @group Materials
*/
class SDRMaterial extends ShaderMaterial {
_brightness = 0;
_contrast = 1;
_saturation = 1;
_exposure = 1;
_toneMapping;
_map;
/**
*
* @param params
*/
constructor({ map, toneMapping }) {
super({
name: 'SDRMaterial',
vertexShader: vertexShader$1,
fragmentShader: fragmentShader$1,
uniforms: {
map: { value: map },
brightness: { value: 0 },
contrast: { value: 1 },
saturation: { value: 1 },
exposure: { value: 1 }
},
blending: NoBlending,
depthTest: false,
depthWrite: false
});
this._map = map;
this.toneMapping = this._toneMapping = toneMapping || ACESFilmicToneMapping;
this.needsUpdate = true;
this.uniformsNeedUpdate = true;
}
get toneMapping() { return this._toneMapping; }
set toneMapping(value) {
let valid = false;
switch (value) {
case ACESFilmicToneMapping:
this.defines.TONEMAPPING_FUNCTION = 'ACESFilmicToneMapping';
valid = true;
break;
case ReinhardToneMapping:
this.defines.TONEMAPPING_FUNCTION = 'ReinhardToneMapping';
valid = true;
break;
case CineonToneMapping:
this.defines.TONEMAPPING_FUNCTION = 'CineonToneMapping';
valid = true;
break;
case LinearToneMapping:
this.defines.TONEMAPPING_FUNCTION = 'LinearToneMapping';
valid = true;
break;
case AgXToneMapping:
this.defines.TONEMAPPING_FUNCTION = 'AgXToneMapping';
valid = true;
break;
case NeutralToneMapping:
this.defines.TONEMAPPING_FUNCTION = 'NeutralToneMapping';
valid = true;
break;
default:
console.error(`Unsupported toneMapping: ${value}. Using LinearToneMapping.`);
this.defines.TONEMAPPING_FUNCTION = 'LinearToneMapping';
this._toneMapping = LinearToneMapping;
}
if (valid) {
this._toneMapping = value;
}
this.needsUpdate = true;
}
get brightness() { return this._brightness; }
set brightness(value) {
this._brightness = value;
this.uniforms.brightness.value = value;
}
get contrast() { return this._contrast; }
set contrast(value) {
this._contrast = value;
this.uniforms.contrast.value = value;
}
get saturation() { return this._saturation; }
set saturation(value) {
this._saturation = value;
this.uniforms.saturation.value = value;
}
get exposure() { return this._exposure; }
set exposure(value) {
this._exposure = value;
this.uniforms.exposure.value = value;
}
get map() { return this._map; }
set map(value) {
this._map = value;
this.uniforms.map.value = value;
}
}
/**
* Renders an SDR Representation of an HDR Image
*
* @category Encoding Functions
* @group Encoding Functions
*
* @param hdrTexture The HDR image to be rendered
* @param renderer (optional) WebGLRenderer to use during the rendering, a disposable renderer will be create and destroyed if this is not provided.
* @param toneMapping (optional) Tone mapping to be applied to the SDR Rendition
* @param renderTargetOptions (optional) Options to use when creating the output renderTarget
* @throws {Error} if the WebGLRenderer fails to render the SDR image
*/
const getSDRRendition = (hdrTexture, renderer, toneMapping, renderTargetOptions) => {
hdrTexture.needsUpdate = true;
const quadRenderer = new QuadRenderer({
width: hdrTexture.image.width,
height: hdrTexture.image.height,
type: UnsignedByteType,
colorSpace: SRGBColorSpace,
material: new SDRMaterial({ map: hdrTexture, toneMapping }),
renderer,
renderTargetOptions
});
try {
quadRenderer.render();
}
catch (e) {
quadRenderer.disposeOnDemandRenderer();
throw e;
}
return quadRenderer;
};
/**
* Encodes a Gainmap starting from an HDR file.
*
* @remarks
* if you do not pass a `renderer` parameter
* you must manually dispose the result
* ```js
* const encodingResult = await encode({ ... })
* // do something with the buffers
* const sdr = encodingResult.sdr.getArray()
* const gainMap = encodingResult.gainMap.getArray()
* // after that
* encodingResult.sdr.dispose()
* encodingResult.gainMap.dispose()
* ```
*
* @category Encoding Functions
* @group Encoding Functions
*
* @example
* import { encode, findTextureMinMax } from '@monogrid/gainmap-js'
* import { EXRLoader } from 'three/examples/jsm/loaders/EXRLoader.js'
*
* // load an HDR file
* const loader = new EXRLoader()
* const image = await loader.loadAsync('image.exr')
*
* // find RAW RGB Max value of a texture
* const textureMax = findTextureMinMax(image)
*
* // Encode the gainmap
* const encodingResult = encode({
* image,
* // this will encode the full HDR range
* maxContentBoost: Math.max.apply(this, textureMax)
* })
* // can be re-encoded after changing parameters
* encodingResult.sdr.material.exposure = 0.9
* encodingResult.sdr.render()
* // or
* encodingResult.gainMap.material.gamma = [1.1, 1.1, 1.1]
* encodingResult.gainMap.render()
*
* // do something with encodingResult.gainMap.toArray()
* // and encodingResult.sdr.toArray()
*
* // renderers must be manually disposed
* encodingResult.sdr.dispose()
* encodingResult.gainMap.dispose()
*
* @param params Encoding Parameters
* @returns
*/
const encode = (params) => {
const { image, renderer } = params;
const dataTexture = getDataTexture(image);
const sdr = getSDRRendition(dataTexture, renderer, params.toneMapping, params.renderTargetOptions);
const gainMapRenderer = getGainMap({
...params,
image: dataTexture,
sdr,
renderer: sdr.renderer // reuse the same (maybe disposable?) renderer
});
return {
sdr,
gainMap: gainMapRenderer,
hdr: dataTexture,
getMetadata: () => {
return {
gainMapMax: gainMapRenderer.material.gainMapMax,
gainMapMin: gainMapRenderer.material.gainMapMin,
gamma: gainMapRenderer.material.gamma,
hdrCapacityMax: gainMapRenderer.material.hdrCapacityMax,
hdrCapacityMin: gainMapRenderer.material.hdrCapacityMin,
offsetHdr: gainMapRenderer.material.offsetHdr,
offsetSdr: gainMapRenderer.material.offsetSdr
};
}
};
};
/**
* Encodes a Gainmap starting from an HDR file into compressed file formats (`image/jpeg`, `image/webp` or `image/png`).
*
* Uses {@link encode} internally, then pipes the results to {@link compress}.
*
* @remarks
* if a `renderer` parameter is not provided
* This function will automatically dispose its "disposable"
* renderer, no need to dispose it manually later
*
* @category Encoding Functions
* @group Encoding Functions
* @example
* import { encodeAndCompress, findTextureMinMax } from '@monogrid/gainmap-js'
* import { encodeJPEGMetadata } from '@monogrid/gainmap-js/libultrahdr'
* import { EXRLoader } from 'three/examples/jsm/loaders/EXRLoader.js'
*
* // load an HDR file
* const loader = new EXRLoader()
* const image = await loader.loadAsync('image.exr')
*
* // find RAW RGB Max value of a texture
* const textureMax = findTextureMinMax(image)
*
* // Encode the gainmap
* const encodingResult = await encodeAndCompress({
* image,
* maxContentBoost: Math.max.apply(this, textureMax),
* mimeType: 'image/jpeg'
* })
*
* // embed the compressed images + metadata into a single
* // JPEG file
* const jpeg = encodeJPEGMetadata({
* ...encodingResult,
* sdr: encodingResult.sdr,
* gainMap: encodingResult.gainMap
* })
*
* // `jpeg` will be an `Uint8Array` which can be saved somewhere
*
*
* @param params Encoding Parameters
* @throws {Error} if the browser does not support [createImageBitmap](https://caniuse.com/createimagebitmap)
*/
const encodeAndCompress = async (params) => {
const encodingResult = encode(params);
const { mimeType, quality, flipY, withWorker } = params;
let compressResult;
let rawSDR;
let rawGainMap;
const sdrImageData = new ImageData(encodingResult.sdr.toArray(), encodingResult.sdr.width, encodingResult.sdr.height);
const gainMapImageData = new ImageData(encodingResult.gainMap.toArray(), encodingResult.gainMap.width, encodingResult.gainMap.height);
if (withWorker) {
const workerResult = await Promise.all([
withWorker.compress({
source: sdrImageData,
mimeType,
quality,
flipY
}),
withWorker.compress({
source: gainMapImageData,
mimeType,
quality,
flipY
})
]);
compressResult = workerResult;
rawSDR = workerResult[0].source;
rawGainMap = workerResult[1].source;
}
else {
compressResult = await Promise.all([
compress({
source: sdrImageData,
mimeType,
quality,
flipY
}),
compress({
source: gainMapImageData,
mimeType,
quality,
flipY
})
]);
rawSDR = sdrImageData.data;
rawGainMap = gainMapImageData.data;
}
encodingResult.sdr.dispose();
encodingResult.gainMap.dispose();
return {
...encodingResult,
...encodingResult.getMetadata(),
sdr: compressResult[0],
gainMap: compressResult[1],
rawSDR,
rawGainMap
};
};
const vertexShader = /* glsl */ `
varying vec2 vUv;
void main() {
vUv = uv;
gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
}
`;
const fragmentShader = /* glsl */ `
precision mediump float;
#ifndef CELL_SIZE
#define CELL_SIZE 2
#endif
#ifndef COMPARE_FUNCTION
#define COMPARE_FUNCTION max
#endif
#ifndef INITIAL_VALUE
#define INITIAL_VALUE 0
#endif
uniform sampler2D map;
uniform vec2 u_srcResolution;
varying vec2 vUv;
void main() {
// compute the first pixel the source cell
vec2 srcPixel = floor(gl_FragCoord.xy) * float(CELL_SIZE);
// one pixel in source
vec2 onePixel = vec2(1) / u_srcResolution;
// uv for first pixel in cell. +0.5 for center of pixel
vec2 uv = (srcPixel + 0.5) * onePixel;
vec4 resultColor = vec4(INITIAL_VALUE);
for (int y = 0; y < CELL_SIZE; ++y) {
for (int x = 0; x < CELL_SIZE; ++x) {
resultColor = COMPARE_FUNCTION(resultColor, texture2D(map, uv + vec2(x, y) * onePixel));
}
}
gl_FragColor = resultColor;
}
`;
/**
*
* @category Utility
* @group Utility
*
* @param image
* @param mode
* @param renderer
* @returns
*/
const findTextureMinMax = (image, mode = 'max', renderer) => {
const srcTex = getDataTexture(image);
// check if texture has a valid format
if (srcTex.format !== AlphaFormat &&
srcTex.format !== RGBFormat &&
srcTex.format !== RGBAFormat &&
srcTex.format !== DepthFormat &&
srcTex.format !== DepthStencilFormat &&
srcTex.format !== RedFormat &&
srcTex.format !== RedIntegerFormat &&
srcTex.format !== RGFormat &&
srcTex.format !== RGIntegerFormat &&
srcTex.format !== RGBIntegerFormat &&
srcTex.format !== RGBAIntegerFormat) {
throw new Error('Unsupported texture format');
}
const cellSize = 2;
const mat = new ShaderMaterial({
vertexShader,
fragmentShader,
uniforms: {
u_srcResolution: { value: new Vector2(srcTex.image.width, srcTex.image.height) },
map: { value: srcTex }
},
defines: {
CELL_SIZE: cellSize,
COMPARE_FUNCTION: mode,
INITIAL_VALUE: mode === 'max' ? 0 : 65504 // max half float value
}
});
srcTex.needsUpdate = true;
mat.needsUpdate = true;
let w = srcTex.image.width;
let h = srcTex.image.height;
const quadRenderer = new QuadRenderer({
width: w,
height: h,
type: srcTex.type,
colorSpace: srcTex.colorSpace,
material: mat,
renderer
});
const frameBuffers = [];
while (w > 1 || h > 1) {
w = Math.max(1, (w + cellSize - 1) / cellSize | 0);
h = Math.max(1, (h + cellSize - 1) / cellSize | 0);
const fb = new WebGLRenderTarget(w, h, {
type: quadRenderer.type,
format: srcTex.format,
colorSpace: quadRenderer.colorSpace,
minFilter: NearestFilter,
magFilter: NearestFilter,
wrapS: ClampToEdgeWrapping,
wrapT: ClampToEdgeWrapping,
generateMipmaps: false,
depthBuffer: false,
stencilBuffer: false
});
frameBuffers.push(fb);
}
w = srcTex.image.width;
h = srcTex.image.height;
frameBuffers.forEach((fbi) => {
w = Math.max(1, (w + cellSize - 1) / cellSize | 0);
h = Math.max(1, (h + cellSize - 1) / cellSize | 0);
quadRenderer.renderTarget = fbi;
quadRenderer.render();
mat.uniforms.map.value = fbi.texture;
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
mat.uniforms.u_srcResolution.value.x = w;
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
mat.uniforms.u_srcResolution.value.y = h;
});
const out = quadRenderer.toArray();
quadRenderer.dispose();
frameBuffers.forEach(fb => fb.dispose());
return [
quadRenderer.type === FloatType ? out[0] : DataUtils.fromHalfFloat(out[0]),
quadRenderer.type === FloatType ? out[1] : DataUtils.fromHalfFloat(out[1]),
quadRenderer.type === FloatType ? out[2] : DataUtils.fromHalfFloat(out[2])
];
};
export { GainMapEncoderMaterial, SDRMaterial, compress, encode, encodeAndCompress, findTextureMinMax, getGainMap, getSDRRendition };
File diff suppressed because one or more lines are too long
File diff suppressed because it is too large Load Diff
File diff suppressed because one or more lines are too long
+12
View File
@@ -0,0 +1,12 @@
import { CompressedImage, CompressParameters } from './types';
/**
* Converts a RAW RGBA image buffer into the provided `mimeType` using the provided `quality`
*
* @category Compression
* @group Compression
* @param params
* @throws {Error} if the browser does not support [createImageBitmap](https://caniuse.com/createimagebitmap)
* @throws {Error} if the provided source image cannot be decoded
* @throws {Error} if the function fails to create a canvas context
*/
export declare const compress: (params: CompressParameters) => Promise<CompressedImage>;
+61
View File
@@ -0,0 +1,61 @@
import { CompressedImage, EncodingParametersWithCompression } from './types';
/**
* Encodes a Gainmap starting from an HDR file into compressed file formats (`image/jpeg`, `image/webp` or `image/png`).
*
* Uses {@link encode} internally, then pipes the results to {@link compress}.
*
* @remarks
* if a `renderer` parameter is not provided
* This function will automatically dispose its "disposable"
* renderer, no need to dispose it manually later
*
* @category Encoding Functions
* @group Encoding Functions
* @example
* import { encodeAndCompress, findTextureMinMax } from '@monogrid/gainmap-js'
* import { encodeJPEGMetadata } from '@monogrid/gainmap-js/libultrahdr'
* import { EXRLoader } from 'three/examples/jsm/loaders/EXRLoader.js'
*
* // load an HDR file
* const loader = new EXRLoader()
* const image = await loader.loadAsync('image.exr')
*
* // find RAW RGB Max value of a texture
* const textureMax = findTextureMinMax(image)
*
* // Encode the gainmap
* const encodingResult = await encodeAndCompress({
* image,
* maxContentBoost: Math.max.apply(this, textureMax),
* mimeType: 'image/jpeg'
* })
*
* // embed the compressed images + metadata into a single
* // JPEG file
* const jpeg = encodeJPEGMetadata({
* ...encodingResult,
* sdr: encodingResult.sdr,
* gainMap: encodingResult.gainMap
* })
*
* // `jpeg` will be an `Uint8Array` which can be saved somewhere
*
*
* @param params Encoding Parameters
* @throws {Error} if the browser does not support [createImageBitmap](https://caniuse.com/createimagebitmap)
*/
export declare const encodeAndCompress: (params: EncodingParametersWithCompression) => Promise<{
sdr: CompressedImage;
gainMap: CompressedImage;
rawSDR: Uint8ClampedArray<ArrayBufferLike>;
rawGainMap: Uint8ClampedArray<ArrayBufferLike>;
gamma: [number, number, number];
hdrCapacityMin: number;
hdrCapacityMax: number;
offsetSdr: [number, number, number];
offsetHdr: [number, number, number];
gainMapMin: [number, number, number];
gainMapMax: [number, number, number];
hdr: import("three").DataTexture;
getMetadata: () => import(".").GainMapMetadata;
}>;
+61
View File
@@ -0,0 +1,61 @@
import { GainMapMetadata } from '../core/types';
import { EncodingParametersBase } from './types';
/**
* Encodes a Gainmap starting from an HDR file.
*
* @remarks
* if you do not pass a `renderer` parameter
* you must manually dispose the result
* ```js
* const encodingResult = await encode({ ... })
* // do something with the buffers
* const sdr = encodingResult.sdr.getArray()
* const gainMap = encodingResult.gainMap.getArray()
* // after that
* encodingResult.sdr.dispose()
* encodingResult.gainMap.dispose()
* ```
*
* @category Encoding Functions
* @group Encoding Functions
*
* @example
* import { encode, findTextureMinMax } from '@monogrid/gainmap-js'
* import { EXRLoader } from 'three/examples/jsm/loaders/EXRLoader.js'
*
* // load an HDR file
* const loader = new EXRLoader()
* const image = await loader.loadAsync('image.exr')
*
* // find RAW RGB Max value of a texture
* const textureMax = findTextureMinMax(image)
*
* // Encode the gainmap
* const encodingResult = encode({
* image,
* // this will encode the full HDR range
* maxContentBoost: Math.max.apply(this, textureMax)
* })
* // can be re-encoded after changing parameters
* encodingResult.sdr.material.exposure = 0.9
* encodingResult.sdr.render()
* // or
* encodingResult.gainMap.material.gamma = [1.1, 1.1, 1.1]
* encodingResult.gainMap.render()
*
* // do something with encodingResult.gainMap.toArray()
* // and encodingResult.sdr.toArray()
*
* // renderers must be manually disposed
* encodingResult.sdr.dispose()
* encodingResult.gainMap.dispose()
*
* @param params Encoding Parameters
* @returns
*/
export declare const encode: (params: EncodingParametersBase) => {
sdr: import("../core").QuadRenderer<1009, import(".").SDRMaterial>;
gainMap: import("../core").QuadRenderer<1009, import(".").GainMapEncoderMaterial>;
hdr: import("three").DataTexture;
getMetadata: () => GainMapMetadata;
};
@@ -0,0 +1,15 @@
import { DataTexture, WebGLRenderer } from 'three';
import { EXR } from 'three/examples/jsm/loaders/EXRLoader.js';
import { HDR } from 'three/examples/jsm/loaders/HDRLoader.js';
import { RGBE } from 'three/examples/jsm/loaders/RGBELoader.js';
/**
*
* @category Utility
* @group Utility
*
* @param image
* @param mode
* @param renderer
* @returns
*/
export declare const findTextureMinMax: (image: EXR | RGBE | HDR | DataTexture, mode?: "min" | "max", renderer?: WebGLRenderer) => number[];
+13
View File
@@ -0,0 +1,13 @@
import { QuadRenderer } from '../core/QuadRenderer';
import { GainMapEncoderMaterial } from './materials/GainMapEncoderMaterial';
import { EncodingParametersBase } from './types';
/**
*
* @param params
* @returns
* @category Encoding Functions
* @group Encoding Functions
*/
export declare const getGainMap: (params: {
sdr: InstanceType<typeof QuadRenderer>;
} & EncodingParametersBase) => QuadRenderer<1009, GainMapEncoderMaterial>;
+17
View File
@@ -0,0 +1,17 @@
import { DataTexture, ToneMapping, UnsignedByteType, WebGLRenderer } from 'three';
import { QuadRenderer } from '../core/QuadRenderer';
import { QuadRendererTextureOptions } from '../decode';
import { SDRMaterial } from './materials/SDRMaterial';
/**
* Renders an SDR Representation of an HDR Image
*
* @category Encoding Functions
* @group Encoding Functions
*
* @param hdrTexture The HDR image to be rendered
* @param renderer (optional) WebGLRenderer to use during the rendering, a disposable renderer will be create and destroyed if this is not provided.
* @param toneMapping (optional) Tone mapping to be applied to the SDR Rendition
* @param renderTargetOptions (optional) Options to use when creating the output renderTarget
* @throws {Error} if the WebGLRenderer fails to render the SDR image
*/
export declare const getSDRRendition: (hdrTexture: DataTexture, renderer?: WebGLRenderer, toneMapping?: ToneMapping, renderTargetOptions?: QuadRendererTextureOptions) => InstanceType<typeof QuadRenderer<typeof UnsignedByteType, InstanceType<typeof SDRMaterial>>>;
+10
View File
@@ -0,0 +1,10 @@
export * from '../core/types';
export * from './compress';
export * from './encode';
export * from './encode-and-compress';
export * from './find-texture-min-max';
export * from './get-gainmap';
export * from './get-sdr-rendition';
export * from './materials/GainMapEncoderMaterial';
export * from './materials/SDRMaterial';
export * from './types';
@@ -0,0 +1,70 @@
import { ShaderMaterial, Texture } from 'three';
import { GainmapEncodingParameters } from '../types';
/**
* A Material which is able to encode a gainmap
*
* @category Materials
* @group Materials
*/
export declare class GainMapEncoderMaterial extends ShaderMaterial {
private _minContentBoost;
private _maxContentBoost;
private _offsetSdr;
private _offsetHdr;
private _gamma;
/**
*
* @param params
*/
constructor({ sdr, hdr, offsetSdr, offsetHdr, maxContentBoost, minContentBoost, gamma }: {
sdr: Texture;
hdr: Texture;
} & GainmapEncodingParameters);
/**
* @see {@link GainmapEncodingParameters.gamma}
*/
get gamma(): [number, number, number];
set gamma(value: [number, number, number]);
/**
* @see {@link GainmapEncodingParameters.offsetHdr}
*/
get offsetHdr(): [number, number, number];
set offsetHdr(value: [number, number, number]);
/**
* @see {@link GainmapEncodingParameters.offsetSdr}
*/
get offsetSdr(): [number, number, number];
set offsetSdr(value: [number, number, number]);
/**
* @see {@link GainmapEncodingParameters.minContentBoost}
* @remarks Non logarithmic space
*/
get minContentBoost(): number;
set minContentBoost(value: number);
/**
* @see {@link GainmapEncodingParameters.maxContentBoost}
* @remarks Non logarithmic space
*/
get maxContentBoost(): number;
set maxContentBoost(value: number);
/**
* @see {@link GainMapMetadata.gainMapMin}
* @remarks Logarithmic space
*/
get gainMapMin(): [number, number, number];
/**
* @see {@link GainMapMetadata.gainMapMax}
* @remarks Logarithmic space
*/
get gainMapMax(): [number, number, number];
/**
* @see {@link GainMapMetadata.hdrCapacityMin}
* @remarks Logarithmic space
*/
get hdrCapacityMin(): number;
/**
* @see {@link GainMapMetadata.hdrCapacityMax}
* @remarks Logarithmic space
*/
get hdrCapacityMax(): number;
}
@@ -0,0 +1,35 @@
import { ShaderMaterial, Texture, ToneMapping } from 'three';
/**
* A Material used to adjust the SDR representation of an HDR image
*
* @category Materials
* @group Materials
*/
export declare class SDRMaterial extends ShaderMaterial {
private _brightness;
private _contrast;
private _saturation;
private _exposure;
private _toneMapping;
private _map;
/**
*
* @param params
*/
constructor({ map, toneMapping }: {
map: Texture;
toneMapping?: ToneMapping;
});
get toneMapping(): ToneMapping;
set toneMapping(value: ToneMapping);
get brightness(): number;
set brightness(value: number);
get contrast(): number;
set contrast(value: number);
get saturation(): number;
set saturation(value: number);
get exposure(): number;
set exposure(value: number);
get map(): Texture;
set map(value: Texture);
}
+177
View File
@@ -0,0 +1,177 @@
import { type DataTexture, ToneMapping, WebGLRenderer } from 'three';
import { type EXR } from 'three/examples/jsm/loaders/EXRLoader.js';
import { type HDR } from 'three/examples/jsm/loaders/HDRLoader.js';
import { type RGBE } from 'three/examples/jsm/loaders/RGBELoader.js';
import { QuadRendererTextureOptions } from '../decode';
import { WorkerInterfaceImplementation } from '../worker-types';
/**
* Parameters used by content Creators in order to create a GainMap
* @category Specs
* @group Specs
*/
export type GainmapEncodingParameters = {
/**
* This is the offset to apply to the HDR pixel values during gain map generation and application.
* @defaultValue [1/64, 1/64, 1/64]
*/
offsetHdr?: [number, number, number];
/**
* This is the offset to apply to the SDR pixel values during gain map generation and application
* @defaultValue [1/64, 1/64, 1/64]
*/
offsetSdr?: [number, number, number];
/**
* This value lets the content creator constrain how much darker an image can get, when shown on an HDR display, relative to the SDR rendition.
* This value is a constant for a particular image.
* @defaultValue 1
* @remarks
* * If, for example, the value is 0.5, then for any given pixel, the linear luminance of the displayed HDR rendition must be (at the least) 0.5x the linear luminance of the SDR rendition.
* * In practice, this value is typically equal to or just less than 1.0.
* * Always less than or equal to Max content boost.
*
* Non Logarithmic space
*/
minContentBoost?: number;
/**
* This value lets the content creator constrain how much brighter an image can get, when shown on an HDR display, relative to the SDR rendition.
*
* @remarks
* * This value is a constant for a particular image.
* For example, if the value is four, then for any given pixel, the linear luminance of the
* displayed HDR rendition must be, at the most, 4x the linear luminance of the SDR rendition.
* In practice, this means that the brighter parts of the scene can be shown up to 4x brighter.
* * In practice, this value is typically greater than 1.0.
* * Always greater than or equal to Min content boost.
*
* Non Logarithmic space
*/
maxContentBoost: number;
/**
* This is the gamma to apply to the stored map values.
*
* @defaultValue [1, 1, 1]
* @remarks
* * Typically you can use a gamma of 1.0.
* * You can use a different value if your gain map has a very uneven distribution of log_recovery(x, y) values.
* 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), 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).
* 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).
*/
gamma?: [number, number, number];
};
/**
* Parameters used to Encode a GainMap
*
* @category Encoding Parameters
* @group Encoding Parameters
*/
export type EncodingParametersBase = GainmapEncodingParameters & {
/**
* Input image for encoding, must be an HDR image
*/
image: EXR | RGBE | HDR | DataTexture;
/**
* Optional WebGLRenderer
* @remarks
* will be created and destroyed on demand if not provided.
*/
renderer?: WebGLRenderer;
/**
* Optional tone mapping to apply to the SDR Rendition
* @defaultValue `ACESFilmicToneMapping`
*/
toneMapping?: ToneMapping;
/**
* Options to use when creating the output renderTarget
*/
renderTargetOptions?: QuadRendererTextureOptions;
};
/**
* This library can provide gainmap compressed in these mimeTypes
*
* @category Compression
* @group Compression
*/
export type CompressionMimeType = 'image/png' | 'image/jpeg' | 'image/webp';
/**
* Accepted HDR image buffers, definitions coming from the THREE.js Library types
*
* @category Utility
* @group Utility
*/
export type HDRRawImageBuffer = EXR['data'] | RGBE['data'];
/**
* Raw HDR image data
*
* @category Utility
* @group Utility
*/
export type HDRRawImage = {
data: HDRRawImageBuffer;
width: number;
height: number;
};
/**
* Options for compressing a RAW RGBA image into the specified mimeType
*
* @category Compression
* @group Compression
*/
export type CompressOptions = {
/**
* The mimeType of the output
*/
mimeType: CompressionMimeType;
/**
* Encoding quality [0-1]
*/
quality?: number;
/**
* FlipY of the encoding process
*/
flipY?: boolean;
};
/**
* Data structure representing a compressed image with a mimeType
*
* @category Compression
* @group Compression
*/
export type CompressedImage = {
data: Uint8Array<ArrayBuffer>;
mimeType: CompressionMimeType;
width: number;
height: number;
};
/**
* @category Compression
* @group Compression
*/
export type CompressParameters = CompressOptions & ({
/**
* RAW RGBA Image Data
*/
source: ImageData;
} | {
/**
* Encoded Image Data with a mimeType
*/
source: Uint8Array<ArrayBuffer> | Uint8ClampedArray<ArrayBuffer>;
/**
* mimeType of the encoded input
*/
sourceMimeType: string;
});
/**
* Additional parameters to encode a GainMap compressed with a mimeType
* @category Encoding Parameters
* @group Encoding Parameters
*/
export type EncodingParametersWithCompression = EncodingParametersBase & CompressOptions & {
/**
* Encodes the Gainmap using a Web Worker
*/
withWorker?: WorkerInterfaceImplementation;
};
+1
View File
@@ -0,0 +1 @@
export * from './libultrahdr/encode-jpeg-metadata';
+650
View File
@@ -0,0 +1,650 @@
/**
* @monogrid/gainmap-js v3.4.0
* With ❤️, by MONOGRID <gainmap@monogrid.com>
*/
/**
* JPEG marker constants
* Based on JPEG specification and libultrahdr implementation
*/
/**
* JPEG marker prefix - all markers start with this byte
*/
const MARKER_PREFIX = 0xff;
/**
* JPEG markers
*/
const MARKERS = {
/** Start of Image */
SOI: 0xd8,
/** Application segment 1 (EXIF/XMP) */
APP1: 0xe1,
/** Application segment 2 (ICC/MPF) */
APP2: 0xe2,
/** Start of Scan */
SOS: 0xda};
/**
* XMP namespace identifier for APP1 marker
*/
const XMP_NAMESPACE = 'http://ns.adobe.com/xap/1.0/\0';
/**
* Multi-Picture Format (MPF) generator
* Based on CIPA DC-007 specification and libultrahdr multipictureformat.cpp
*
* MPF is used to embed multiple images in a single JPEG file
*/
/**
* MPF constants from the specification
*/
const MPF_CONSTANTS = {
/** MPF signature "MPF\0" */
SIGNATURE: new Uint8Array([0x4d, 0x50, 0x46, 0x00]),
/** Big endian marker "MM" */
BIG_ENDIAN: new Uint8Array([0x4d, 0x4d]),
/** TIFF magic number */
TIFF_MAGIC: 0x002a,
/** Number of pictures in MPF */
NUM_PICTURES: 2,
/** Number of tags to serialize */
TAG_COUNT: 3,
/** Size of each tag in bytes */
TAG_SIZE: 12,
/** Size of each MP entry in bytes */
MP_ENTRY_SIZE: 16
};
/**
* MPF tag identifiers
*/
const MPF_TAGS = {
/** MPF version tag */
VERSION: 0xb000,
/** Number of images tag */
NUMBER_OF_IMAGES: 0xb001,
/** MP entry tag */
MP_ENTRY: 0xb002
};
/**
* MPF tag types
*/
const MPF_TAG_TYPES = {
/** Undefined type */
UNDEFINED: 7,
/** Unsigned long type */
ULONG: 4
};
/**
* MP entry attributes
*/
const MP_ENTRY_ATTRIBUTES = {
/** JPEG format */
FORMAT_JPEG: 0x00000000,
/** Primary image type */
TYPE_PRIMARY: 0x20000000
};
/**
* MPF version string
*/
const MPF_VERSION = new Uint8Array([0x30, 0x31, 0x30, 0x30]); // "0100"
/**
* Calculate the total size of the MPF structure
*/
function calculateMpfSize() {
return (MPF_CONSTANTS.SIGNATURE.length + // Signature "MPF\0"
2 + // Endianness marker
2 + // TIFF magic number
4 + // Index IFD Offset
2 + // Tag count
MPF_CONSTANTS.TAG_COUNT * MPF_CONSTANTS.TAG_SIZE + // Tags
4 + // Attribute IFD offset
MPF_CONSTANTS.NUM_PICTURES * MPF_CONSTANTS.MP_ENTRY_SIZE // MP Entries
);
}
/**
* Generate MPF (Multi-Picture Format) data structure
*
* @param primaryImageSize - Size of the primary image in bytes
* @param primaryImageOffset - Offset of the primary image (typically 0 for FII - First Individual Image)
* @param secondaryImageSize - Size of the secondary (gain map) image in bytes
* @param secondaryImageOffset - Offset of the secondary image from the MP Endian field
* @returns Uint8Array containing the MPF data
*/
function generateMpf(primaryImageSize, primaryImageOffset, secondaryImageSize, secondaryImageOffset) {
const mpfSize = calculateMpfSize();
const buffer = new ArrayBuffer(mpfSize);
const view = new DataView(buffer);
const uint8View = new Uint8Array(buffer);
let pos = 0;
// Write MPF signature "MPF\0"
uint8View.set(MPF_CONSTANTS.SIGNATURE, pos);
pos += MPF_CONSTANTS.SIGNATURE.length;
// Write endianness marker (big endian "MM")
// Using big endian to match the C++ implementation's USE_BIG_ENDIAN
uint8View.set(MPF_CONSTANTS.BIG_ENDIAN, pos);
const bigEndian = false; // DataView uses little endian by default, so we need to flip this
pos += 2;
// Write TIFF magic number (0x002A)
view.setUint16(pos, MPF_CONSTANTS.TIFF_MAGIC, bigEndian);
pos += 2;
// Set the Index IFD offset
// This offset is from the start of the TIFF header (the endianness marker)
// After: endianness (2) + magic (2) + this offset field (4) = 8 bytes
const indexIfdOffset = 8;
view.setUint32(pos, indexIfdOffset, bigEndian);
pos += 4;
// Write tag count (3 tags: version, number of images, MP entries)
view.setUint16(pos, MPF_CONSTANTS.TAG_COUNT, bigEndian);
pos += 2;
// Write version tag
view.setUint16(pos, MPF_TAGS.VERSION, bigEndian);
pos += 2;
view.setUint16(pos, MPF_TAG_TYPES.UNDEFINED, bigEndian);
pos += 2;
view.setUint32(pos, MPF_VERSION.length, bigEndian);
pos += 4;
uint8View.set(MPF_VERSION, pos);
pos += 4; // Version is 4 bytes, embedded in the tag
// Write number of images tag
view.setUint16(pos, MPF_TAGS.NUMBER_OF_IMAGES, bigEndian);
pos += 2;
view.setUint16(pos, MPF_TAG_TYPES.ULONG, bigEndian);
pos += 2;
view.setUint32(pos, 1, bigEndian); // Count = 1
pos += 4;
view.setUint32(pos, MPF_CONSTANTS.NUM_PICTURES, bigEndian);
pos += 4;
// Write MP entry tag
view.setUint16(pos, MPF_TAGS.MP_ENTRY, bigEndian);
pos += 2;
view.setUint16(pos, MPF_TAG_TYPES.UNDEFINED, bigEndian);
pos += 2;
view.setUint32(pos, MPF_CONSTANTS.MP_ENTRY_SIZE * MPF_CONSTANTS.NUM_PICTURES, bigEndian);
pos += 4;
// Calculate MP entry offset
// The offset is from the start of the MP Endian field (after signature)
// Current position is at the value field of MP Entry tag
const mpEntryOffset = pos - MPF_CONSTANTS.SIGNATURE.length + 4 + 4;
view.setUint32(pos, mpEntryOffset, bigEndian);
pos += 4;
// Write attribute IFD offset (0 = none)
view.setUint32(pos, 0, bigEndian);
pos += 4;
// Write MP entries for primary image
// Attribute format: JPEG (0x00000000) | Type: Primary (0x20000000)
view.setUint32(pos, MP_ENTRY_ATTRIBUTES.FORMAT_JPEG | MP_ENTRY_ATTRIBUTES.TYPE_PRIMARY, bigEndian);
pos += 4;
view.setUint32(pos, primaryImageSize, bigEndian);
pos += 4;
view.setUint32(pos, primaryImageOffset, bigEndian);
pos += 4;
view.setUint16(pos, 0, bigEndian); // Dependent image 1
pos += 2;
view.setUint16(pos, 0, bigEndian); // Dependent image 2
pos += 2;
// Write MP entries for secondary image (gain map)
// Attribute format: JPEG only (no type flag)
view.setUint32(pos, MP_ENTRY_ATTRIBUTES.FORMAT_JPEG, bigEndian);
pos += 4;
view.setUint32(pos, secondaryImageSize, bigEndian);
pos += 4;
view.setUint32(pos, secondaryImageOffset, bigEndian);
pos += 4;
view.setUint16(pos, 0, bigEndian); // Dependent image 1
pos += 2;
view.setUint16(pos, 0, bigEndian); // Dependent image 2
// pos += 2
return uint8View;
}
/**
* XMP metadata generator for gain map images
* Based on libultrahdr jpegrutils.cpp implementation
*/
/**
* Item semantic types
*/
const ITEM_SEMANTIC = {
PRIMARY: 'Primary',
GAIN_MAP: 'GainMap'
};
/**
* MIME type for JPEG images
*/
const MIME_IMAGE_JPEG = 'image/jpeg';
/**
* Escape XML special characters
*/
function escapeXml(str) {
return String(str)
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;')
.replace(/'/g, '&apos;');
}
/**
* Generate XMP metadata for the primary image
*
* This XMP contains:
* - Container directory with references to primary and gain map images
* - Gain map version
* - Item metadata for both images
*
* @param secondaryImageLength - Length of the secondary (gain map) JPEG in bytes
* @param metadata - Gain map metadata
* @returns XMP packet as string
*/
function generateXmpForPrimaryImage(secondaryImageLength, metadata) {
const lines = [];
// XMP packet header
lines.push('<?xpacket begin="" id="W5M0MpCehiHzreSzNTczkc9d"?>');
lines.push('<x:xmpmeta xmlns:x="adobe:ns:meta/" x:xmptk="Adobe XMP Core 5.1.2">');
lines.push(' <rdf:RDF xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#">');
lines.push(' <rdf:Description');
lines.push(' xmlns:Container="http://ns.google.com/photos/1.0/container/"');
lines.push(' xmlns:Item="http://ns.google.com/photos/1.0/container/item/"');
lines.push(' xmlns:hdrgm="http://ns.adobe.com/hdr-gain-map/1.0/"');
lines.push(` hdrgm:Version="${escapeXml(metadata.version)}"`);
lines.push(' rdf:about="">');
// Container directory
lines.push(' <Container:Directory>');
lines.push(' <rdf:Seq>');
// Primary image item
lines.push(' <rdf:li rdf:parseType="Resource">');
lines.push(' <Container:Item');
lines.push(` Item:Semantic="${ITEM_SEMANTIC.PRIMARY}"`);
lines.push(` Item:Mime="${MIME_IMAGE_JPEG}"/>`);
lines.push(' </rdf:li>');
// Gain map image item
lines.push(' <rdf:li rdf:parseType="Resource">');
lines.push(' <Container:Item');
lines.push(` Item:Semantic="${ITEM_SEMANTIC.GAIN_MAP}"`);
lines.push(` Item:Mime="${MIME_IMAGE_JPEG}"`);
lines.push(` Item:Length="${secondaryImageLength}"/>`);
lines.push(' </rdf:li>');
lines.push(' </rdf:Seq>');
lines.push(' </Container:Directory>');
lines.push(' </rdf:Description>');
lines.push(' </rdf:RDF>');
lines.push('</x:xmpmeta>');
lines.push('<?xpacket end="w"?>');
return lines.join('\n');
}
/**
* Generate XMP metadata for the secondary (gain map) image
*
* This XMP contains all the gain map parameters:
* - Version
* - Gain map min/max
* - Gamma
* - Offset SDR/HDR
* - HDR capacity min/max
* - Base rendition flag
*
* @param metadata - Gain map metadata
* @returns XMP packet as string
*/
function generateXmpForSecondaryImage(metadata) {
const lines = [];
// hdrCapacityMin/Max are already in log2 space (from GainMapEncoderMaterial)
// No conversion needed
const hdrCapacityMin = metadata.hdrCapacityMin;
const hdrCapacityMax = metadata.hdrCapacityMax;
// Handle array values - take average if array, or use single value
const getAverage = (val) => {
if (Array.isArray(val)) {
return val.reduce((sum, v) => sum + v, 0) / val.length;
}
return val;
};
const gainMapMinAvg = getAverage(metadata.gainMapMin);
const gainMapMaxAvg = getAverage(metadata.gainMapMax);
const gammaAvg = getAverage(metadata.gamma);
const offsetSdrAvg = getAverage(metadata.offsetSdr);
const offsetHdrAvg = getAverage(metadata.offsetHdr);
// XMP packet header
lines.push('<?xpacket begin="" id="W5M0MpCehiHzreSzNTczkc9d"?>');
lines.push('<x:xmpmeta xmlns:x="adobe:ns:meta/" x:xmptk="Adobe XMP Core 5.1.2">');
lines.push(' <rdf:RDF xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#">');
lines.push(' <rdf:Description');
lines.push(' xmlns:hdrgm="http://ns.adobe.com/hdr-gain-map/1.0/"');
lines.push(` hdrgm:Version="${escapeXml(metadata.version)}"`);
lines.push(` hdrgm:GainMapMin="${escapeXml(gainMapMinAvg)}"`);
lines.push(` hdrgm:GainMapMax="${escapeXml(gainMapMaxAvg)}"`);
lines.push(` hdrgm:Gamma="${escapeXml(gammaAvg)}"`);
lines.push(` hdrgm:OffsetSDR="${escapeXml(offsetSdrAvg)}"`);
lines.push(` hdrgm:OffsetHDR="${escapeXml(offsetHdrAvg)}"`);
lines.push(` hdrgm:HDRCapacityMin="${escapeXml(hdrCapacityMin)}"`);
lines.push(` hdrgm:HDRCapacityMax="${escapeXml(hdrCapacityMax)}"`);
lines.push(' hdrgm:BaseRenditionIsHDR="False"');
lines.push(' rdf:about=""/>');
lines.push(' </rdf:RDF>');
lines.push('</x:xmpmeta>');
lines.push('<?xpacket end="w"?>');
return lines.join('\n');
}
/**
* JPEG assembler for creating JPEG-R (JPEG with gain map) files
* Based on libultrahdr jpegr.cpp implementation
*/
/**
* Extract EXIF data from a JPEG if present
*
* @param jpegData - JPEG file data
* @returns Object containing EXIF data and position, or null if not found
*/
function extractExif(jpegData) {
const view = new DataView(jpegData.buffer, jpegData.byteOffset, jpegData.byteLength);
// Check for JPEG SOI marker
if (view.getUint8(0) !== MARKER_PREFIX || view.getUint8(1) !== MARKERS.SOI) {
return null;
}
let offset = 2;
const EXIF_SIGNATURE = 'Exif\0\0';
while (offset < jpegData.length - 1) {
// Check for marker prefix
if (view.getUint8(offset) !== MARKER_PREFIX) {
break;
}
const marker = view.getUint8(offset + 1);
// Check for SOS (Start of Scan) - end of metadata
if (marker === MARKERS.SOS) {
break;
}
// Check for APP1 marker (EXIF/XMP)
if (marker === MARKERS.APP1) {
const length = view.getUint16(offset + 2, false); // Big endian
const dataStart = offset + 4;
// Check if this APP1 contains EXIF
let isExif = true;
for (let i = 0; i < EXIF_SIGNATURE.length; i++) {
if (dataStart + i >= jpegData.length || jpegData[dataStart + i] !== EXIF_SIGNATURE.charCodeAt(i)) {
isExif = false;
break;
}
}
if (isExif) {
// Found EXIF data
const exifSize = length - 2; // Length includes the 2-byte length field itself
const exifData = jpegData.slice(dataStart, dataStart + exifSize);
return {
data: exifData,
pos: offset,
size: length + 2 // Include marker (2 bytes) + length (2 bytes) + data
};
}
}
// Move to next marker
const length = view.getUint16(offset + 2, false);
offset += 2 + length;
}
return null;
}
/**
* Copy JPEG data without EXIF segment
*
* @param jpegData - Original JPEG data
* @param exifPos - Position of EXIF segment
* @param exifSize - Size of EXIF segment (including marker and length)
* @returns JPEG data without EXIF
*/
function copyJpegWithoutExif(jpegData, exifPos, exifSize) {
const newSize = jpegData.length - exifSize;
const result = new Uint8Array(newSize);
// Copy data before EXIF
result.set(jpegData.subarray(0, exifPos), 0);
// Copy data after EXIF
result.set(jpegData.subarray(exifPos + exifSize), exifPos);
return result;
}
/**
* Write a JPEG marker and its data
*
* @param buffer - Target buffer
* @param pos - Current position in buffer
* @param marker - Marker type (without 0xFF prefix)
* @param data - Data to write after marker
* @returns New position after writing
*/
function writeMarker(buffer, pos, marker, data) {
const view = new DataView(buffer.buffer, buffer.byteOffset, buffer.byteLength);
// Write marker
view.setUint8(pos++, MARKER_PREFIX);
view.setUint8(pos++, marker);
// Write data if present
if (data && data.length > 0) {
// Write length (big endian, includes the 2-byte length field itself)
const length = data.length + 2;
view.setUint16(pos, length, false);
pos += 2;
// Write data
buffer.set(data, pos);
pos += data.length;
}
return pos;
}
/**
* Assemble a JPEG-R file (JPEG with embedded gain map)
*
* The structure is:
* 1. Primary image:
* - SOI
* - APP1 (EXIF if present)
* - APP1 (XMP with gain map metadata)
* - APP2 (ICC profile if present)
* - APP2 (MPF data)
* - Rest of primary JPEG data
* 2. Secondary image (gain map):
* - SOI
* - APP1 (XMP with gain map parameters)
* - Rest of gain map JPEG data
*
* @param options - Assembly options
* @returns Complete JPEG-R file as Uint8Array
*/
function assembleJpegWithGainMap(options) {
const { sdr, gainMap, metadata, exif: externalExif, icc } = options;
// Validate input
if (sdr.mimeType !== 'image/jpeg') {
throw new Error('SDR image must be JPEG format');
}
if (gainMap.mimeType !== 'image/jpeg') {
throw new Error('Gain map image must be JPEG format');
}
// Check for EXIF in primary image
const exifFromJpeg = extractExif(sdr.data);
if (exifFromJpeg && externalExif) {
throw new Error('Primary image already contains EXIF data, cannot add external EXIF');
}
// Prepare primary JPEG (remove embedded EXIF if present)
let primaryJpegData = sdr.data;
let exifData = externalExif;
if (exifFromJpeg) {
primaryJpegData = copyJpegWithoutExif(sdr.data, exifFromJpeg.pos, exifFromJpeg.size);
exifData = exifFromJpeg.data;
}
// Generate XMP for secondary image
const xmpSecondary = generateXmpForSecondaryImage(metadata);
const xmpSecondaryBytes = new TextEncoder().encode(xmpSecondary);
// Calculate secondary image size
// 2 bytes SOI + 2 bytes marker + 2 bytes length field + namespace + XMP data + gain map data (without SOI)
const namespaceBytes = new TextEncoder().encode(XMP_NAMESPACE);
const secondaryImageSize = 2 + 2 + 2 + namespaceBytes.length + xmpSecondaryBytes.length + (gainMap.data.length - 2);
// Generate XMP for primary image
const xmpPrimary = generateXmpForPrimaryImage(secondaryImageSize, metadata);
const xmpPrimaryBytes = new TextEncoder().encode(xmpPrimary);
const xmpPrimaryData = new Uint8Array(namespaceBytes.length + xmpPrimaryBytes.length);
xmpPrimaryData.set(namespaceBytes, 0);
xmpPrimaryData.set(xmpPrimaryBytes, namespaceBytes.length);
// Calculate MPF size and offset
const mpfLength = calculateMpfSize();
// Calculate total size
let totalSize = 2; // SOI
if (exifData)
totalSize += 2 + 2 + exifData.length; // APP1 + length + EXIF
totalSize += 2 + 2 + xmpPrimaryData.length; // APP1 + length + XMP primary
if (icc)
totalSize += 2 + 2 + icc.length; // APP2 + length + ICC
totalSize += 2 + 2 + mpfLength; // APP2 + length + MPF
totalSize += primaryJpegData.length - 2; // Primary JPEG without SOI
totalSize += secondaryImageSize; // Secondary image
// Calculate offsets for MPF
const primaryImageSize = totalSize - secondaryImageSize;
// Offset is from MP Endian field (after APP2 marker + length + MPF signature)
const secondaryImageOffset = primaryImageSize - (2 + // SOI
(exifData ? 2 + 2 + exifData.length : 0) +
2 + 2 + xmpPrimaryData.length +
(icc ? 2 + 2 + icc.length : 0) +
2 + 2 + 4 // APP2 marker + length + MPF signature
);
// Generate MPF data
const mpfDataActual = generateMpf(primaryImageSize, 0, secondaryImageSize, secondaryImageOffset);
// Allocate output buffer
const output = new Uint8Array(totalSize);
let pos = 0;
// === PRIMARY IMAGE ===
// Write SOI
pos = writeMarker(output, pos, MARKERS.SOI);
// Write EXIF if present
if (exifData) {
pos = writeMarker(output, pos, MARKERS.APP1, exifData);
}
// Write XMP for primary image (already created above)
pos = writeMarker(output, pos, MARKERS.APP1, xmpPrimaryData);
// Write ICC profile if present
if (icc) {
pos = writeMarker(output, pos, MARKERS.APP2, icc);
}
// Write MPF
pos = writeMarker(output, pos, MARKERS.APP2, mpfDataActual);
// Write rest of primary JPEG (skip SOI)
output.set(primaryJpegData.subarray(2), pos);
pos += primaryJpegData.length - 2;
// === SECONDARY IMAGE (GAIN MAP) ===
// Write SOI
pos = writeMarker(output, pos, MARKERS.SOI);
// Write XMP for secondary image
const xmpSecondaryData = new Uint8Array(namespaceBytes.length + xmpSecondaryBytes.length);
xmpSecondaryData.set(namespaceBytes, 0);
xmpSecondaryData.set(xmpSecondaryBytes, namespaceBytes.length);
pos = writeMarker(output, pos, MARKERS.APP1, xmpSecondaryData);
// Write rest of gain map JPEG (skip SOI)
output.set(gainMap.data.subarray(2), pos);
// pos += gainMap.data.length - 2
return output;
}
/**
* Encapsulates a Gainmap into a single JPEG file (aka: JPEG-R) with the base map
* as the sdr visualization and the gainMap encoded into a MPF (Multi-Picture Format) tag.
*
* @category Encoding
* @group Encoding
*
* @example
* import { compress, encode, findTextureMinMax } from '@monogrid/gainmap-js'
* import { encodeJPEGMetadata } from '@monogrid/gainmap-js/libultrahdr'
* import { EXRLoader } from 'three/examples/jsm/loaders/EXRLoader.js'
*
* // load an HDR file
* const loader = new EXRLoader()
* const image = await loader.loadAsync('image.exr')
*
* // find RAW RGB Max value of a texture
* const textureMax = findTextureMinMax(image)
*
* // Encode the gainmap
* const encodingResult = encode({
* image,
* maxContentBoost: Math.max.apply(this, textureMax)
* })
*
* // obtain the RAW RGBA SDR buffer and create an ImageData
* const sdrImageData = new ImageData(
* encodingResult.sdr.toArray(),
* encodingResult.sdr.width,
* encodingResult.sdr.height
* )
* // obtain the RAW RGBA Gain map buffer and create an ImageData
* const gainMapImageData = new ImageData(
* encodingResult.gainMap.toArray(),
* encodingResult.gainMap.width,
* encodingResult.gainMap.height
* )
*
* // parallel compress the RAW buffers into the specified mimeType
* const mimeType = 'image/jpeg'
* const quality = 0.9
*
* const [sdr, gainMap] = await Promise.all([
* compress({
* source: sdrImageData,
* mimeType,
* quality,
* flipY: true // output needs to be flipped
* }),
* compress({
* source: gainMapImageData,
* mimeType,
* quality,
* flipY: true // output needs to be flipped
* })
* ])
*
* // obtain the metadata which will be embedded into
* // and XMP tag inside the final JPEG file
* const metadata = encodingResult.getMetadata()
*
* // embed the compressed images + metadata into a single
* // JPEG file
* const jpeg = encodeJPEGMetadata({
* ...encodingResult,
* ...metadata,
* sdr,
* gainMap
* })
*
* // `jpeg` will be an `Uint8Array` which can be saved somewhere
*
*
* @param encodingResult - Encoding result containing SDR image, gain map image, and metadata
* @returns A Uint8Array representing a JPEG-R file
* @throws {Error} If `encodingResult.sdr.mimeType !== 'image/jpeg'`
* @throws {Error} If `encodingResult.gainMap.mimeType !== 'image/jpeg'`
*/
const encodeJPEGMetadata = (encodingResult) => {
// Validate input
if (encodingResult.sdr.mimeType !== 'image/jpeg') {
throw new Error('This function expects an SDR image compressed in jpeg');
}
if (encodingResult.gainMap.mimeType !== 'image/jpeg') {
throw new Error('This function expects a GainMap image compressed in jpeg');
}
// Prepare metadata with proper conversions
// The XMP generator handles the log2 conversion internally for gain map min/max values
const metadata = {
version: '1.0',
gainMapMin: encodingResult.gainMapMin,
gainMapMax: encodingResult.gainMapMax,
gamma: encodingResult.gamma,
offsetSdr: encodingResult.offsetSdr,
offsetHdr: encodingResult.offsetHdr,
hdrCapacityMin: encodingResult.hdrCapacityMin,
hdrCapacityMax: encodingResult.hdrCapacityMax,
minContentBoost: Array.isArray(encodingResult.gainMapMin)
? Math.pow(2, encodingResult.gainMapMin.reduce((a, b) => a + b, 0) / encodingResult.gainMapMin.length)
: Math.pow(2, encodingResult.gainMapMin),
maxContentBoost: Array.isArray(encodingResult.gainMapMax)
? Math.pow(2, encodingResult.gainMapMax.reduce((a, b) => a + b, 0) / encodingResult.gainMapMax.length)
: Math.pow(2, encodingResult.gainMapMax)
};
// Assemble the JPEG with gain map using pure JavaScript
return assembleJpegWithGainMap({
sdr: encodingResult.sdr,
gainMap: encodingResult.gainMap,
metadata
});
};
export { encodeJPEGMetadata };
File diff suppressed because one or more lines are too long
+658
View File
@@ -0,0 +1,658 @@
/**
* @monogrid/gainmap-js v3.4.0
* With ❤️, by MONOGRID <gainmap@monogrid.com>
*/
(function (global, factory) {
typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports) :
typeof define === 'function' && define.amd ? define(['exports'], factory) :
(global = typeof globalThis !== 'undefined' ? globalThis : global || self, factory(global.libultrahdr = {}));
})(this, (function (exports) { 'use strict';
/**
* JPEG marker constants
* Based on JPEG specification and libultrahdr implementation
*/
/**
* JPEG marker prefix - all markers start with this byte
*/
const MARKER_PREFIX = 0xff;
/**
* JPEG markers
*/
const MARKERS = {
/** Start of Image */
SOI: 0xd8,
/** Application segment 1 (EXIF/XMP) */
APP1: 0xe1,
/** Application segment 2 (ICC/MPF) */
APP2: 0xe2,
/** Start of Scan */
SOS: 0xda};
/**
* XMP namespace identifier for APP1 marker
*/
const XMP_NAMESPACE = 'http://ns.adobe.com/xap/1.0/\0';
/**
* Multi-Picture Format (MPF) generator
* Based on CIPA DC-007 specification and libultrahdr multipictureformat.cpp
*
* MPF is used to embed multiple images in a single JPEG file
*/
/**
* MPF constants from the specification
*/
const MPF_CONSTANTS = {
/** MPF signature "MPF\0" */
SIGNATURE: new Uint8Array([0x4d, 0x50, 0x46, 0x00]),
/** Big endian marker "MM" */
BIG_ENDIAN: new Uint8Array([0x4d, 0x4d]),
/** TIFF magic number */
TIFF_MAGIC: 0x002a,
/** Number of pictures in MPF */
NUM_PICTURES: 2,
/** Number of tags to serialize */
TAG_COUNT: 3,
/** Size of each tag in bytes */
TAG_SIZE: 12,
/** Size of each MP entry in bytes */
MP_ENTRY_SIZE: 16
};
/**
* MPF tag identifiers
*/
const MPF_TAGS = {
/** MPF version tag */
VERSION: 0xb000,
/** Number of images tag */
NUMBER_OF_IMAGES: 0xb001,
/** MP entry tag */
MP_ENTRY: 0xb002
};
/**
* MPF tag types
*/
const MPF_TAG_TYPES = {
/** Undefined type */
UNDEFINED: 7,
/** Unsigned long type */
ULONG: 4
};
/**
* MP entry attributes
*/
const MP_ENTRY_ATTRIBUTES = {
/** JPEG format */
FORMAT_JPEG: 0x00000000,
/** Primary image type */
TYPE_PRIMARY: 0x20000000
};
/**
* MPF version string
*/
const MPF_VERSION = new Uint8Array([0x30, 0x31, 0x30, 0x30]); // "0100"
/**
* Calculate the total size of the MPF structure
*/
function calculateMpfSize() {
return (MPF_CONSTANTS.SIGNATURE.length + // Signature "MPF\0"
2 + // Endianness marker
2 + // TIFF magic number
4 + // Index IFD Offset
2 + // Tag count
MPF_CONSTANTS.TAG_COUNT * MPF_CONSTANTS.TAG_SIZE + // Tags
4 + // Attribute IFD offset
MPF_CONSTANTS.NUM_PICTURES * MPF_CONSTANTS.MP_ENTRY_SIZE // MP Entries
);
}
/**
* Generate MPF (Multi-Picture Format) data structure
*
* @param primaryImageSize - Size of the primary image in bytes
* @param primaryImageOffset - Offset of the primary image (typically 0 for FII - First Individual Image)
* @param secondaryImageSize - Size of the secondary (gain map) image in bytes
* @param secondaryImageOffset - Offset of the secondary image from the MP Endian field
* @returns Uint8Array containing the MPF data
*/
function generateMpf(primaryImageSize, primaryImageOffset, secondaryImageSize, secondaryImageOffset) {
const mpfSize = calculateMpfSize();
const buffer = new ArrayBuffer(mpfSize);
const view = new DataView(buffer);
const uint8View = new Uint8Array(buffer);
let pos = 0;
// Write MPF signature "MPF\0"
uint8View.set(MPF_CONSTANTS.SIGNATURE, pos);
pos += MPF_CONSTANTS.SIGNATURE.length;
// Write endianness marker (big endian "MM")
// Using big endian to match the C++ implementation's USE_BIG_ENDIAN
uint8View.set(MPF_CONSTANTS.BIG_ENDIAN, pos);
const bigEndian = false; // DataView uses little endian by default, so we need to flip this
pos += 2;
// Write TIFF magic number (0x002A)
view.setUint16(pos, MPF_CONSTANTS.TIFF_MAGIC, bigEndian);
pos += 2;
// Set the Index IFD offset
// This offset is from the start of the TIFF header (the endianness marker)
// After: endianness (2) + magic (2) + this offset field (4) = 8 bytes
const indexIfdOffset = 8;
view.setUint32(pos, indexIfdOffset, bigEndian);
pos += 4;
// Write tag count (3 tags: version, number of images, MP entries)
view.setUint16(pos, MPF_CONSTANTS.TAG_COUNT, bigEndian);
pos += 2;
// Write version tag
view.setUint16(pos, MPF_TAGS.VERSION, bigEndian);
pos += 2;
view.setUint16(pos, MPF_TAG_TYPES.UNDEFINED, bigEndian);
pos += 2;
view.setUint32(pos, MPF_VERSION.length, bigEndian);
pos += 4;
uint8View.set(MPF_VERSION, pos);
pos += 4; // Version is 4 bytes, embedded in the tag
// Write number of images tag
view.setUint16(pos, MPF_TAGS.NUMBER_OF_IMAGES, bigEndian);
pos += 2;
view.setUint16(pos, MPF_TAG_TYPES.ULONG, bigEndian);
pos += 2;
view.setUint32(pos, 1, bigEndian); // Count = 1
pos += 4;
view.setUint32(pos, MPF_CONSTANTS.NUM_PICTURES, bigEndian);
pos += 4;
// Write MP entry tag
view.setUint16(pos, MPF_TAGS.MP_ENTRY, bigEndian);
pos += 2;
view.setUint16(pos, MPF_TAG_TYPES.UNDEFINED, bigEndian);
pos += 2;
view.setUint32(pos, MPF_CONSTANTS.MP_ENTRY_SIZE * MPF_CONSTANTS.NUM_PICTURES, bigEndian);
pos += 4;
// Calculate MP entry offset
// The offset is from the start of the MP Endian field (after signature)
// Current position is at the value field of MP Entry tag
const mpEntryOffset = pos - MPF_CONSTANTS.SIGNATURE.length + 4 + 4;
view.setUint32(pos, mpEntryOffset, bigEndian);
pos += 4;
// Write attribute IFD offset (0 = none)
view.setUint32(pos, 0, bigEndian);
pos += 4;
// Write MP entries for primary image
// Attribute format: JPEG (0x00000000) | Type: Primary (0x20000000)
view.setUint32(pos, MP_ENTRY_ATTRIBUTES.FORMAT_JPEG | MP_ENTRY_ATTRIBUTES.TYPE_PRIMARY, bigEndian);
pos += 4;
view.setUint32(pos, primaryImageSize, bigEndian);
pos += 4;
view.setUint32(pos, primaryImageOffset, bigEndian);
pos += 4;
view.setUint16(pos, 0, bigEndian); // Dependent image 1
pos += 2;
view.setUint16(pos, 0, bigEndian); // Dependent image 2
pos += 2;
// Write MP entries for secondary image (gain map)
// Attribute format: JPEG only (no type flag)
view.setUint32(pos, MP_ENTRY_ATTRIBUTES.FORMAT_JPEG, bigEndian);
pos += 4;
view.setUint32(pos, secondaryImageSize, bigEndian);
pos += 4;
view.setUint32(pos, secondaryImageOffset, bigEndian);
pos += 4;
view.setUint16(pos, 0, bigEndian); // Dependent image 1
pos += 2;
view.setUint16(pos, 0, bigEndian); // Dependent image 2
// pos += 2
return uint8View;
}
/**
* XMP metadata generator for gain map images
* Based on libultrahdr jpegrutils.cpp implementation
*/
/**
* Item semantic types
*/
const ITEM_SEMANTIC = {
PRIMARY: 'Primary',
GAIN_MAP: 'GainMap'
};
/**
* MIME type for JPEG images
*/
const MIME_IMAGE_JPEG = 'image/jpeg';
/**
* Escape XML special characters
*/
function escapeXml(str) {
return String(str)
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;')
.replace(/'/g, '&apos;');
}
/**
* Generate XMP metadata for the primary image
*
* This XMP contains:
* - Container directory with references to primary and gain map images
* - Gain map version
* - Item metadata for both images
*
* @param secondaryImageLength - Length of the secondary (gain map) JPEG in bytes
* @param metadata - Gain map metadata
* @returns XMP packet as string
*/
function generateXmpForPrimaryImage(secondaryImageLength, metadata) {
const lines = [];
// XMP packet header
lines.push('<?xpacket begin="" id="W5M0MpCehiHzreSzNTczkc9d"?>');
lines.push('<x:xmpmeta xmlns:x="adobe:ns:meta/" x:xmptk="Adobe XMP Core 5.1.2">');
lines.push(' <rdf:RDF xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#">');
lines.push(' <rdf:Description');
lines.push(' xmlns:Container="http://ns.google.com/photos/1.0/container/"');
lines.push(' xmlns:Item="http://ns.google.com/photos/1.0/container/item/"');
lines.push(' xmlns:hdrgm="http://ns.adobe.com/hdr-gain-map/1.0/"');
lines.push(` hdrgm:Version="${escapeXml(metadata.version)}"`);
lines.push(' rdf:about="">');
// Container directory
lines.push(' <Container:Directory>');
lines.push(' <rdf:Seq>');
// Primary image item
lines.push(' <rdf:li rdf:parseType="Resource">');
lines.push(' <Container:Item');
lines.push(` Item:Semantic="${ITEM_SEMANTIC.PRIMARY}"`);
lines.push(` Item:Mime="${MIME_IMAGE_JPEG}"/>`);
lines.push(' </rdf:li>');
// Gain map image item
lines.push(' <rdf:li rdf:parseType="Resource">');
lines.push(' <Container:Item');
lines.push(` Item:Semantic="${ITEM_SEMANTIC.GAIN_MAP}"`);
lines.push(` Item:Mime="${MIME_IMAGE_JPEG}"`);
lines.push(` Item:Length="${secondaryImageLength}"/>`);
lines.push(' </rdf:li>');
lines.push(' </rdf:Seq>');
lines.push(' </Container:Directory>');
lines.push(' </rdf:Description>');
lines.push(' </rdf:RDF>');
lines.push('</x:xmpmeta>');
lines.push('<?xpacket end="w"?>');
return lines.join('\n');
}
/**
* Generate XMP metadata for the secondary (gain map) image
*
* This XMP contains all the gain map parameters:
* - Version
* - Gain map min/max
* - Gamma
* - Offset SDR/HDR
* - HDR capacity min/max
* - Base rendition flag
*
* @param metadata - Gain map metadata
* @returns XMP packet as string
*/
function generateXmpForSecondaryImage(metadata) {
const lines = [];
// hdrCapacityMin/Max are already in log2 space (from GainMapEncoderMaterial)
// No conversion needed
const hdrCapacityMin = metadata.hdrCapacityMin;
const hdrCapacityMax = metadata.hdrCapacityMax;
// Handle array values - take average if array, or use single value
const getAverage = (val) => {
if (Array.isArray(val)) {
return val.reduce((sum, v) => sum + v, 0) / val.length;
}
return val;
};
const gainMapMinAvg = getAverage(metadata.gainMapMin);
const gainMapMaxAvg = getAverage(metadata.gainMapMax);
const gammaAvg = getAverage(metadata.gamma);
const offsetSdrAvg = getAverage(metadata.offsetSdr);
const offsetHdrAvg = getAverage(metadata.offsetHdr);
// XMP packet header
lines.push('<?xpacket begin="" id="W5M0MpCehiHzreSzNTczkc9d"?>');
lines.push('<x:xmpmeta xmlns:x="adobe:ns:meta/" x:xmptk="Adobe XMP Core 5.1.2">');
lines.push(' <rdf:RDF xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#">');
lines.push(' <rdf:Description');
lines.push(' xmlns:hdrgm="http://ns.adobe.com/hdr-gain-map/1.0/"');
lines.push(` hdrgm:Version="${escapeXml(metadata.version)}"`);
lines.push(` hdrgm:GainMapMin="${escapeXml(gainMapMinAvg)}"`);
lines.push(` hdrgm:GainMapMax="${escapeXml(gainMapMaxAvg)}"`);
lines.push(` hdrgm:Gamma="${escapeXml(gammaAvg)}"`);
lines.push(` hdrgm:OffsetSDR="${escapeXml(offsetSdrAvg)}"`);
lines.push(` hdrgm:OffsetHDR="${escapeXml(offsetHdrAvg)}"`);
lines.push(` hdrgm:HDRCapacityMin="${escapeXml(hdrCapacityMin)}"`);
lines.push(` hdrgm:HDRCapacityMax="${escapeXml(hdrCapacityMax)}"`);
lines.push(' hdrgm:BaseRenditionIsHDR="False"');
lines.push(' rdf:about=""/>');
lines.push(' </rdf:RDF>');
lines.push('</x:xmpmeta>');
lines.push('<?xpacket end="w"?>');
return lines.join('\n');
}
/**
* JPEG assembler for creating JPEG-R (JPEG with gain map) files
* Based on libultrahdr jpegr.cpp implementation
*/
/**
* Extract EXIF data from a JPEG if present
*
* @param jpegData - JPEG file data
* @returns Object containing EXIF data and position, or null if not found
*/
function extractExif(jpegData) {
const view = new DataView(jpegData.buffer, jpegData.byteOffset, jpegData.byteLength);
// Check for JPEG SOI marker
if (view.getUint8(0) !== MARKER_PREFIX || view.getUint8(1) !== MARKERS.SOI) {
return null;
}
let offset = 2;
const EXIF_SIGNATURE = 'Exif\0\0';
while (offset < jpegData.length - 1) {
// Check for marker prefix
if (view.getUint8(offset) !== MARKER_PREFIX) {
break;
}
const marker = view.getUint8(offset + 1);
// Check for SOS (Start of Scan) - end of metadata
if (marker === MARKERS.SOS) {
break;
}
// Check for APP1 marker (EXIF/XMP)
if (marker === MARKERS.APP1) {
const length = view.getUint16(offset + 2, false); // Big endian
const dataStart = offset + 4;
// Check if this APP1 contains EXIF
let isExif = true;
for (let i = 0; i < EXIF_SIGNATURE.length; i++) {
if (dataStart + i >= jpegData.length || jpegData[dataStart + i] !== EXIF_SIGNATURE.charCodeAt(i)) {
isExif = false;
break;
}
}
if (isExif) {
// Found EXIF data
const exifSize = length - 2; // Length includes the 2-byte length field itself
const exifData = jpegData.slice(dataStart, dataStart + exifSize);
return {
data: exifData,
pos: offset,
size: length + 2 // Include marker (2 bytes) + length (2 bytes) + data
};
}
}
// Move to next marker
const length = view.getUint16(offset + 2, false);
offset += 2 + length;
}
return null;
}
/**
* Copy JPEG data without EXIF segment
*
* @param jpegData - Original JPEG data
* @param exifPos - Position of EXIF segment
* @param exifSize - Size of EXIF segment (including marker and length)
* @returns JPEG data without EXIF
*/
function copyJpegWithoutExif(jpegData, exifPos, exifSize) {
const newSize = jpegData.length - exifSize;
const result = new Uint8Array(newSize);
// Copy data before EXIF
result.set(jpegData.subarray(0, exifPos), 0);
// Copy data after EXIF
result.set(jpegData.subarray(exifPos + exifSize), exifPos);
return result;
}
/**
* Write a JPEG marker and its data
*
* @param buffer - Target buffer
* @param pos - Current position in buffer
* @param marker - Marker type (without 0xFF prefix)
* @param data - Data to write after marker
* @returns New position after writing
*/
function writeMarker(buffer, pos, marker, data) {
const view = new DataView(buffer.buffer, buffer.byteOffset, buffer.byteLength);
// Write marker
view.setUint8(pos++, MARKER_PREFIX);
view.setUint8(pos++, marker);
// Write data if present
if (data && data.length > 0) {
// Write length (big endian, includes the 2-byte length field itself)
const length = data.length + 2;
view.setUint16(pos, length, false);
pos += 2;
// Write data
buffer.set(data, pos);
pos += data.length;
}
return pos;
}
/**
* Assemble a JPEG-R file (JPEG with embedded gain map)
*
* The structure is:
* 1. Primary image:
* - SOI
* - APP1 (EXIF if present)
* - APP1 (XMP with gain map metadata)
* - APP2 (ICC profile if present)
* - APP2 (MPF data)
* - Rest of primary JPEG data
* 2. Secondary image (gain map):
* - SOI
* - APP1 (XMP with gain map parameters)
* - Rest of gain map JPEG data
*
* @param options - Assembly options
* @returns Complete JPEG-R file as Uint8Array
*/
function assembleJpegWithGainMap(options) {
const { sdr, gainMap, metadata, exif: externalExif, icc } = options;
// Validate input
if (sdr.mimeType !== 'image/jpeg') {
throw new Error('SDR image must be JPEG format');
}
if (gainMap.mimeType !== 'image/jpeg') {
throw new Error('Gain map image must be JPEG format');
}
// Check for EXIF in primary image
const exifFromJpeg = extractExif(sdr.data);
if (exifFromJpeg && externalExif) {
throw new Error('Primary image already contains EXIF data, cannot add external EXIF');
}
// Prepare primary JPEG (remove embedded EXIF if present)
let primaryJpegData = sdr.data;
let exifData = externalExif;
if (exifFromJpeg) {
primaryJpegData = copyJpegWithoutExif(sdr.data, exifFromJpeg.pos, exifFromJpeg.size);
exifData = exifFromJpeg.data;
}
// Generate XMP for secondary image
const xmpSecondary = generateXmpForSecondaryImage(metadata);
const xmpSecondaryBytes = new TextEncoder().encode(xmpSecondary);
// Calculate secondary image size
// 2 bytes SOI + 2 bytes marker + 2 bytes length field + namespace + XMP data + gain map data (without SOI)
const namespaceBytes = new TextEncoder().encode(XMP_NAMESPACE);
const secondaryImageSize = 2 + 2 + 2 + namespaceBytes.length + xmpSecondaryBytes.length + (gainMap.data.length - 2);
// Generate XMP for primary image
const xmpPrimary = generateXmpForPrimaryImage(secondaryImageSize, metadata);
const xmpPrimaryBytes = new TextEncoder().encode(xmpPrimary);
const xmpPrimaryData = new Uint8Array(namespaceBytes.length + xmpPrimaryBytes.length);
xmpPrimaryData.set(namespaceBytes, 0);
xmpPrimaryData.set(xmpPrimaryBytes, namespaceBytes.length);
// Calculate MPF size and offset
const mpfLength = calculateMpfSize();
// Calculate total size
let totalSize = 2; // SOI
if (exifData)
totalSize += 2 + 2 + exifData.length; // APP1 + length + EXIF
totalSize += 2 + 2 + xmpPrimaryData.length; // APP1 + length + XMP primary
if (icc)
totalSize += 2 + 2 + icc.length; // APP2 + length + ICC
totalSize += 2 + 2 + mpfLength; // APP2 + length + MPF
totalSize += primaryJpegData.length - 2; // Primary JPEG without SOI
totalSize += secondaryImageSize; // Secondary image
// Calculate offsets for MPF
const primaryImageSize = totalSize - secondaryImageSize;
// Offset is from MP Endian field (after APP2 marker + length + MPF signature)
const secondaryImageOffset = primaryImageSize - (2 + // SOI
(exifData ? 2 + 2 + exifData.length : 0) +
2 + 2 + xmpPrimaryData.length +
(icc ? 2 + 2 + icc.length : 0) +
2 + 2 + 4 // APP2 marker + length + MPF signature
);
// Generate MPF data
const mpfDataActual = generateMpf(primaryImageSize, 0, secondaryImageSize, secondaryImageOffset);
// Allocate output buffer
const output = new Uint8Array(totalSize);
let pos = 0;
// === PRIMARY IMAGE ===
// Write SOI
pos = writeMarker(output, pos, MARKERS.SOI);
// Write EXIF if present
if (exifData) {
pos = writeMarker(output, pos, MARKERS.APP1, exifData);
}
// Write XMP for primary image (already created above)
pos = writeMarker(output, pos, MARKERS.APP1, xmpPrimaryData);
// Write ICC profile if present
if (icc) {
pos = writeMarker(output, pos, MARKERS.APP2, icc);
}
// Write MPF
pos = writeMarker(output, pos, MARKERS.APP2, mpfDataActual);
// Write rest of primary JPEG (skip SOI)
output.set(primaryJpegData.subarray(2), pos);
pos += primaryJpegData.length - 2;
// === SECONDARY IMAGE (GAIN MAP) ===
// Write SOI
pos = writeMarker(output, pos, MARKERS.SOI);
// Write XMP for secondary image
const xmpSecondaryData = new Uint8Array(namespaceBytes.length + xmpSecondaryBytes.length);
xmpSecondaryData.set(namespaceBytes, 0);
xmpSecondaryData.set(xmpSecondaryBytes, namespaceBytes.length);
pos = writeMarker(output, pos, MARKERS.APP1, xmpSecondaryData);
// Write rest of gain map JPEG (skip SOI)
output.set(gainMap.data.subarray(2), pos);
// pos += gainMap.data.length - 2
return output;
}
/**
* Encapsulates a Gainmap into a single JPEG file (aka: JPEG-R) with the base map
* as the sdr visualization and the gainMap encoded into a MPF (Multi-Picture Format) tag.
*
* @category Encoding
* @group Encoding
*
* @example
* import { compress, encode, findTextureMinMax } from '@monogrid/gainmap-js'
* import { encodeJPEGMetadata } from '@monogrid/gainmap-js/libultrahdr'
* import { EXRLoader } from 'three/examples/jsm/loaders/EXRLoader.js'
*
* // load an HDR file
* const loader = new EXRLoader()
* const image = await loader.loadAsync('image.exr')
*
* // find RAW RGB Max value of a texture
* const textureMax = findTextureMinMax(image)
*
* // Encode the gainmap
* const encodingResult = encode({
* image,
* maxContentBoost: Math.max.apply(this, textureMax)
* })
*
* // obtain the RAW RGBA SDR buffer and create an ImageData
* const sdrImageData = new ImageData(
* encodingResult.sdr.toArray(),
* encodingResult.sdr.width,
* encodingResult.sdr.height
* )
* // obtain the RAW RGBA Gain map buffer and create an ImageData
* const gainMapImageData = new ImageData(
* encodingResult.gainMap.toArray(),
* encodingResult.gainMap.width,
* encodingResult.gainMap.height
* )
*
* // parallel compress the RAW buffers into the specified mimeType
* const mimeType = 'image/jpeg'
* const quality = 0.9
*
* const [sdr, gainMap] = await Promise.all([
* compress({
* source: sdrImageData,
* mimeType,
* quality,
* flipY: true // output needs to be flipped
* }),
* compress({
* source: gainMapImageData,
* mimeType,
* quality,
* flipY: true // output needs to be flipped
* })
* ])
*
* // obtain the metadata which will be embedded into
* // and XMP tag inside the final JPEG file
* const metadata = encodingResult.getMetadata()
*
* // embed the compressed images + metadata into a single
* // JPEG file
* const jpeg = encodeJPEGMetadata({
* ...encodingResult,
* ...metadata,
* sdr,
* gainMap
* })
*
* // `jpeg` will be an `Uint8Array` which can be saved somewhere
*
*
* @param encodingResult - Encoding result containing SDR image, gain map image, and metadata
* @returns A Uint8Array representing a JPEG-R file
* @throws {Error} If `encodingResult.sdr.mimeType !== 'image/jpeg'`
* @throws {Error} If `encodingResult.gainMap.mimeType !== 'image/jpeg'`
*/
const encodeJPEGMetadata = (encodingResult) => {
// Validate input
if (encodingResult.sdr.mimeType !== 'image/jpeg') {
throw new Error('This function expects an SDR image compressed in jpeg');
}
if (encodingResult.gainMap.mimeType !== 'image/jpeg') {
throw new Error('This function expects a GainMap image compressed in jpeg');
}
// Prepare metadata with proper conversions
// The XMP generator handles the log2 conversion internally for gain map min/max values
const metadata = {
version: '1.0',
gainMapMin: encodingResult.gainMapMin,
gainMapMax: encodingResult.gainMapMax,
gamma: encodingResult.gamma,
offsetSdr: encodingResult.offsetSdr,
offsetHdr: encodingResult.offsetHdr,
hdrCapacityMin: encodingResult.hdrCapacityMin,
hdrCapacityMax: encodingResult.hdrCapacityMax,
minContentBoost: Array.isArray(encodingResult.gainMapMin)
? Math.pow(2, encodingResult.gainMapMin.reduce((a, b) => a + b, 0) / encodingResult.gainMapMin.length)
: Math.pow(2, encodingResult.gainMapMin),
maxContentBoost: Array.isArray(encodingResult.gainMapMax)
? Math.pow(2, encodingResult.gainMapMax.reduce((a, b) => a + b, 0) / encodingResult.gainMapMax.length)
: Math.pow(2, encodingResult.gainMapMax)
};
// Assemble the JPEG with gain map using pure JavaScript
return assembleJpegWithGainMap({
sdr: encodingResult.sdr,
gainMap: encodingResult.gainMap,
metadata
});
};
exports.encodeJPEGMetadata = encodeJPEGMetadata;
}));
File diff suppressed because one or more lines are too long
@@ -0,0 +1,84 @@
import { GainMapMetadata } from '../core/types';
import { type CompressedImage } from '../encode/types';
/**
* Encapsulates a Gainmap into a single JPEG file (aka: JPEG-R) with the base map
* as the sdr visualization and the gainMap encoded into a MPF (Multi-Picture Format) tag.
*
* @category Encoding
* @group Encoding
*
* @example
* import { compress, encode, findTextureMinMax } from '@monogrid/gainmap-js'
* import { encodeJPEGMetadata } from '@monogrid/gainmap-js/libultrahdr'
* import { EXRLoader } from 'three/examples/jsm/loaders/EXRLoader.js'
*
* // load an HDR file
* const loader = new EXRLoader()
* const image = await loader.loadAsync('image.exr')
*
* // find RAW RGB Max value of a texture
* const textureMax = findTextureMinMax(image)
*
* // Encode the gainmap
* const encodingResult = encode({
* image,
* maxContentBoost: Math.max.apply(this, textureMax)
* })
*
* // obtain the RAW RGBA SDR buffer and create an ImageData
* const sdrImageData = new ImageData(
* encodingResult.sdr.toArray(),
* encodingResult.sdr.width,
* encodingResult.sdr.height
* )
* // obtain the RAW RGBA Gain map buffer and create an ImageData
* const gainMapImageData = new ImageData(
* encodingResult.gainMap.toArray(),
* encodingResult.gainMap.width,
* encodingResult.gainMap.height
* )
*
* // parallel compress the RAW buffers into the specified mimeType
* const mimeType = 'image/jpeg'
* const quality = 0.9
*
* const [sdr, gainMap] = await Promise.all([
* compress({
* source: sdrImageData,
* mimeType,
* quality,
* flipY: true // output needs to be flipped
* }),
* compress({
* source: gainMapImageData,
* mimeType,
* quality,
* flipY: true // output needs to be flipped
* })
* ])
*
* // obtain the metadata which will be embedded into
* // and XMP tag inside the final JPEG file
* const metadata = encodingResult.getMetadata()
*
* // embed the compressed images + metadata into a single
* // JPEG file
* const jpeg = encodeJPEGMetadata({
* ...encodingResult,
* ...metadata,
* sdr,
* gainMap
* })
*
* // `jpeg` will be an `Uint8Array` which can be saved somewhere
*
*
* @param encodingResult - Encoding result containing SDR image, gain map image, and metadata
* @returns A Uint8Array representing a JPEG-R file
* @throws {Error} If `encodingResult.sdr.mimeType !== 'image/jpeg'`
* @throws {Error} If `encodingResult.gainMap.mimeType !== 'image/jpeg'`
*/
export declare const encodeJPEGMetadata: (encodingResult: GainMapMetadata & {
sdr: CompressedImage;
gainMap: CompressedImage;
}) => Uint8Array<ArrayBuffer>;
+41
View File
@@ -0,0 +1,41 @@
/**
* JPEG assembler for creating JPEG-R (JPEG with gain map) files
* Based on libultrahdr jpegr.cpp implementation
*/
import { GainMapMetadataExtended } from '../core/types';
import { type CompressedImage } from '../encode/types';
/**
* Options for assembling a JPEG with gain map
*/
export interface AssembleJpegOptions {
/** Primary (SDR) JPEG image */
sdr: CompressedImage;
/** Gain map JPEG image */
gainMap: CompressedImage;
/** Gain map metadata */
metadata: GainMapMetadataExtended;
/** Optional EXIF data to embed */
exif?: Uint8Array<ArrayBuffer>;
/** Optional ICC color profile */
icc?: Uint8Array<ArrayBuffer>;
}
/**
* Assemble a JPEG-R file (JPEG with embedded gain map)
*
* The structure is:
* 1. Primary image:
* - SOI
* - APP1 (EXIF if present)
* - APP1 (XMP with gain map metadata)
* - APP2 (ICC profile if present)
* - APP2 (MPF data)
* - Rest of primary JPEG data
* 2. Secondary image (gain map):
* - SOI
* - APP1 (XMP with gain map parameters)
* - Rest of gain map JPEG data
*
* @param options - Assembly options
* @returns Complete JPEG-R file as Uint8Array
*/
export declare function assembleJpegWithGainMap(options: AssembleJpegOptions): Uint8Array<ArrayBuffer>;
+47
View File
@@ -0,0 +1,47 @@
/**
* JPEG marker constants
* Based on JPEG specification and libultrahdr implementation
*/
/**
* JPEG marker prefix - all markers start with this byte
*/
export declare const MARKER_PREFIX = 255;
/**
* JPEG markers
*/
export declare const MARKERS: {
/** Start of Image */
readonly SOI: 216;
/** End of Image */
readonly EOI: 217;
/** Application segment 0 */
readonly APP0: 224;
/** Application segment 1 (EXIF/XMP) */
readonly APP1: 225;
/** Application segment 2 (ICC/MPF) */
readonly APP2: 226;
/** Start of Scan */
readonly SOS: 218;
/** Define Quantization Table */
readonly DQT: 219;
/** Define Huffman Table */
readonly DHT: 196;
/** Start of Frame (baseline DCT) */
readonly SOF0: 192;
};
/**
* XMP namespace identifier for APP1 marker
*/
export declare const XMP_NAMESPACE = "http://ns.adobe.com/xap/1.0/\0";
/**
* EXIF identifier for APP1 marker
*/
export declare const EXIF_IDENTIFIER = "Exif\0\0";
/**
* MPF signature for APP2 marker
*/
export declare const MPF_SIGNATURE = "MPF\0";
/**
* ICC profile identifier for APP2 marker
*/
export declare const ICC_IDENTIFIER = "ICC_PROFILE\0";
+20
View File
@@ -0,0 +1,20 @@
/**
* Multi-Picture Format (MPF) generator
* Based on CIPA DC-007 specification and libultrahdr multipictureformat.cpp
*
* MPF is used to embed multiple images in a single JPEG file
*/
/**
* Calculate the total size of the MPF structure
*/
export declare function calculateMpfSize(): number;
/**
* Generate MPF (Multi-Picture Format) data structure
*
* @param primaryImageSize - Size of the primary image in bytes
* @param primaryImageOffset - Offset of the primary image (typically 0 for FII - First Individual Image)
* @param secondaryImageSize - Size of the secondary (gain map) image in bytes
* @param secondaryImageOffset - Offset of the secondary image from the MP Endian field
* @returns Uint8Array containing the MPF data
*/
export declare function generateMpf(primaryImageSize: number, primaryImageOffset: number, secondaryImageSize: number, secondaryImageOffset: number): Uint8Array<ArrayBuffer>;
+33
View File
@@ -0,0 +1,33 @@
/**
* XMP metadata generator for gain map images
* Based on libultrahdr jpegrutils.cpp implementation
*/
import { type GainMapMetadataExtended } from '../core/types';
/**
* Generate XMP metadata for the primary image
*
* This XMP contains:
* - Container directory with references to primary and gain map images
* - Gain map version
* - Item metadata for both images
*
* @param secondaryImageLength - Length of the secondary (gain map) JPEG in bytes
* @param metadata - Gain map metadata
* @returns XMP packet as string
*/
export declare function generateXmpForPrimaryImage(secondaryImageLength: number, metadata: GainMapMetadataExtended): string;
/**
* Generate XMP metadata for the secondary (gain map) image
*
* This XMP contains all the gain map parameters:
* - Version
* - Gain map min/max
* - Gamma
* - Offset SDR/HDR
* - HDR capacity min/max
* - Base rendition flag
*
* @param metadata - Gain map metadata
* @returns XMP packet as string
*/
export declare function generateXmpForSecondaryImage(metadata: GainMapMetadataExtended): string;
+301
View File
@@ -0,0 +1,301 @@
Name: is-promise
Version: 2.2.2
License: MIT
Private: false
Description: Test whether an object looks like a promises-a+ promise
Repository: https://github.com/then/is-promise.git
Author: ForbesLindesay
License Copyright:
===
Copyright (c) 2014 Forbes Lindesay
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
---
Name: promise-worker-transferable
Version: 1.0.4
License: Apache-2.0
Private: false
Description: Communicate with a Web Worker using Promises, allowing transferList
Repository: git+https://github.com/terikon/promise-worker-transferable.git
Homepage: https://github.com/terikon/promise-worker-transferable#readme
Author: Roman Viskin <npm@terikon.com>
License Copyright:
===
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "[]"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright [yyyy] [name of copyright owner]
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
---
Name: immediate
Version: 3.0.6
License: MIT
Private: false
Description: A cross browser microtask library
Repository: git://github.com/calvinmetcalf/immediate.git
Contributors:
Domenic Denicola <domenic@domenicdenicola.com> (http://domenicdenicola.com)
Donavon West <github@donavon.com> (http://donavon.com)
Yaffle
Calvin Metcalf <calvin.metcalf@gmail.com>
License Copyright:
===
Copyright (c) 2012 Barnesandnoble.com, llc, Donavon West, Domenic Denicola, Brian Cavalier
Permission is hereby granted, free of charge, to any person obtaining
a copy of this software and associated documentation files (the
"Software"), to deal in the Software without restriction, including
without limitation the rights to use, copy, modify, merge, publish,
distribute, sublicense, and/or sell copies of the Software, and to
permit persons to whom the Software is furnished to do so, subject to
the following conditions:
The above copyright notice and this permission notice shall be
included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
---
Name: lie
Version: 3.3.0
License: MIT
Private: false
Description: A basic but performant promise implementation
Repository: https://github.com/calvinmetcalf/lie.git
License Copyright:
===
#Copyright (c) 2014-2018 Calvin Metcalf, Jordan Harband
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
**THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.**
+26
View File
@@ -0,0 +1,26 @@
import { type PromiseWorkerType, type WorkerInterfaceImplementation } from './worker-types';
export * from './worker-types';
/**
* Wraps a Regular worker into a `PromiseWorker`
*
* @param worker
* @returns
*/
export declare const getPromiseWorker: (worker: Worker) => PromiseWorkerType;
/**
* Returns an interface where methods of the worker can be called by the host site
*
* @example
* // this assumes a vite-like bundler understands the `?worker` import
* import GainMapWorker from '@monogrid/gainmap-js/worker?worker'
* import { getPromiseWorker, getWorkerInterface } from '@monogrid/gainmap-js/worker-interface'
*
* // turn our Worker into a PromiseWorker
* const promiseWorker = getPromiseWorker(new GainMapWorker())
* // get the interface
* const workerInterface = getWorkerInterface(promiseWorker)
*
* @param worker
* @returns
*/
export declare const getWorkerInterface: (worker: PromiseWorkerType) => WorkerInterfaceImplementation;
+512
View File
@@ -0,0 +1,512 @@
/**
* @monogrid/gainmap-js v3.4.0
* With ❤️, by MONOGRID <gainmap@monogrid.com>
*/
import { c as commonjsGlobal, g as getDefaultExportFromCjs } from './_commonjsHelpers-v3qvwW2J.js';
var lib$1;
var hasRequiredLib$1;
function requireLib$1 () {
if (hasRequiredLib$1) return lib$1;
hasRequiredLib$1 = 1;
var Mutation = commonjsGlobal.MutationObserver || commonjsGlobal.WebKitMutationObserver;
var scheduleDrain;
if (process.browser) {
if (Mutation) {
var called = 0;
var observer = new Mutation(nextTick);
var element = commonjsGlobal.document.createTextNode('');
observer.observe(element, {
characterData: true
});
scheduleDrain = function () {
element.data = (called = ++called % 2);
};
} else if (!commonjsGlobal.setImmediate && typeof commonjsGlobal.MessageChannel !== 'undefined') {
var channel = new commonjsGlobal.MessageChannel();
channel.port1.onmessage = nextTick;
scheduleDrain = function () {
channel.port2.postMessage(0);
};
} else if ('document' in commonjsGlobal && 'onreadystatechange' in commonjsGlobal.document.createElement('script')) {
scheduleDrain = function () {
// Create a <script> element; its readystatechange event will be fired asynchronously once it is inserted
// into the document. Do so, thus queuing up the task. Remember to clean up once it's been called.
var scriptEl = commonjsGlobal.document.createElement('script');
scriptEl.onreadystatechange = function () {
nextTick();
scriptEl.onreadystatechange = null;
scriptEl.parentNode.removeChild(scriptEl);
scriptEl = null;
};
commonjsGlobal.document.documentElement.appendChild(scriptEl);
};
} else {
scheduleDrain = function () {
setTimeout(nextTick, 0);
};
}
} else {
scheduleDrain = function () {
process.nextTick(nextTick);
};
}
var draining;
var queue = [];
//named nextTick for less confusing stack traces
function nextTick() {
draining = true;
var i, oldQueue;
var len = queue.length;
while (len) {
oldQueue = queue;
queue = [];
i = -1;
while (++i < len) {
oldQueue[i]();
}
len = queue.length;
}
draining = false;
}
lib$1 = immediate;
function immediate(task) {
if (queue.push(task) === 1 && !draining) {
scheduleDrain();
}
}
return lib$1;
}
var lib;
var hasRequiredLib;
function requireLib () {
if (hasRequiredLib) return lib;
hasRequiredLib = 1;
var immediate = requireLib$1();
/* istanbul ignore next */
function INTERNAL() {}
var handlers = {};
var REJECTED = ['REJECTED'];
var FULFILLED = ['FULFILLED'];
var PENDING = ['PENDING'];
/* istanbul ignore else */
if (!process.browser) {
// in which we actually take advantage of JS scoping
var UNHANDLED = ['UNHANDLED'];
}
lib = Promise;
function Promise(resolver) {
if (typeof resolver !== 'function') {
throw new TypeError('resolver must be a function');
}
this.state = PENDING;
this.queue = [];
this.outcome = void 0;
/* istanbul ignore else */
if (!process.browser) {
this.handled = UNHANDLED;
}
if (resolver !== INTERNAL) {
safelyResolveThenable(this, resolver);
}
}
Promise.prototype.finally = function (callback) {
if (typeof callback !== 'function') {
return this;
}
var p = this.constructor;
return this.then(resolve, reject);
function resolve(value) {
function yes () {
return value;
}
return p.resolve(callback()).then(yes);
}
function reject(reason) {
function no () {
throw reason;
}
return p.resolve(callback()).then(no);
}
};
Promise.prototype.catch = function (onRejected) {
return this.then(null, onRejected);
};
Promise.prototype.then = function (onFulfilled, onRejected) {
if (typeof onFulfilled !== 'function' && this.state === FULFILLED ||
typeof onRejected !== 'function' && this.state === REJECTED) {
return this;
}
var promise = new this.constructor(INTERNAL);
/* istanbul ignore else */
if (!process.browser) {
if (this.handled === UNHANDLED) {
this.handled = null;
}
}
if (this.state !== PENDING) {
var resolver = this.state === FULFILLED ? onFulfilled : onRejected;
unwrap(promise, resolver, this.outcome);
} else {
this.queue.push(new QueueItem(promise, onFulfilled, onRejected));
}
return promise;
};
function QueueItem(promise, onFulfilled, onRejected) {
this.promise = promise;
if (typeof onFulfilled === 'function') {
this.onFulfilled = onFulfilled;
this.callFulfilled = this.otherCallFulfilled;
}
if (typeof onRejected === 'function') {
this.onRejected = onRejected;
this.callRejected = this.otherCallRejected;
}
}
QueueItem.prototype.callFulfilled = function (value) {
handlers.resolve(this.promise, value);
};
QueueItem.prototype.otherCallFulfilled = function (value) {
unwrap(this.promise, this.onFulfilled, value);
};
QueueItem.prototype.callRejected = function (value) {
handlers.reject(this.promise, value);
};
QueueItem.prototype.otherCallRejected = function (value) {
unwrap(this.promise, this.onRejected, value);
};
function unwrap(promise, func, value) {
immediate(function () {
var returnValue;
try {
returnValue = func(value);
} catch (e) {
return handlers.reject(promise, e);
}
if (returnValue === promise) {
handlers.reject(promise, new TypeError('Cannot resolve promise with itself'));
} else {
handlers.resolve(promise, returnValue);
}
});
}
handlers.resolve = function (self, value) {
var result = tryCatch(getThen, value);
if (result.status === 'error') {
return handlers.reject(self, result.value);
}
var thenable = result.value;
if (thenable) {
safelyResolveThenable(self, thenable);
} else {
self.state = FULFILLED;
self.outcome = value;
var i = -1;
var len = self.queue.length;
while (++i < len) {
self.queue[i].callFulfilled(value);
}
}
return self;
};
handlers.reject = function (self, error) {
self.state = REJECTED;
self.outcome = error;
/* istanbul ignore else */
if (!process.browser) {
if (self.handled === UNHANDLED) {
immediate(function () {
if (self.handled === UNHANDLED) {
process.emit('unhandledRejection', error, self);
}
});
}
}
var i = -1;
var len = self.queue.length;
while (++i < len) {
self.queue[i].callRejected(error);
}
return self;
};
function getThen(obj) {
// Make sure we only access the accessor once as required by the spec
var then = obj && obj.then;
if (obj && (typeof obj === 'object' || typeof obj === 'function') && typeof then === 'function') {
return function appyThen() {
then.apply(obj, arguments);
};
}
}
function safelyResolveThenable(self, thenable) {
// Either fulfill, reject or reject with error
var called = false;
function onError(value) {
if (called) {
return;
}
called = true;
handlers.reject(self, value);
}
function onSuccess(value) {
if (called) {
return;
}
called = true;
handlers.resolve(self, value);
}
function tryToUnwrap() {
thenable(onSuccess, onError);
}
var result = tryCatch(tryToUnwrap);
if (result.status === 'error') {
onError(result.value);
}
}
function tryCatch(func, value) {
var out = {};
try {
out.value = func(value);
out.status = 'success';
} catch (e) {
out.status = 'error';
out.value = e;
}
return out;
}
Promise.resolve = resolve;
function resolve(value) {
if (value instanceof this) {
return value;
}
return handlers.resolve(new this(INTERNAL), value);
}
Promise.reject = reject;
function reject(reason) {
var promise = new this(INTERNAL);
return handlers.reject(promise, reason);
}
Promise.all = all;
function all(iterable) {
var self = this;
if (Object.prototype.toString.call(iterable) !== '[object Array]') {
return this.reject(new TypeError('must be an array'));
}
var len = iterable.length;
var called = false;
if (!len) {
return this.resolve([]);
}
var values = new Array(len);
var resolved = 0;
var i = -1;
var promise = new this(INTERNAL);
while (++i < len) {
allResolver(iterable[i], i);
}
return promise;
function allResolver(value, i) {
self.resolve(value).then(resolveFromAll, function (error) {
if (!called) {
called = true;
handlers.reject(promise, error);
}
});
function resolveFromAll(outValue) {
values[i] = outValue;
if (++resolved === len && !called) {
called = true;
handlers.resolve(promise, values);
}
}
}
}
Promise.race = race;
function race(iterable) {
var self = this;
if (Object.prototype.toString.call(iterable) !== '[object Array]') {
return this.reject(new TypeError('must be an array'));
}
var len = iterable.length;
var called = false;
if (!len) {
return this.resolve([]);
}
var i = -1;
var promise = new this(INTERNAL);
while (++i < len) {
resolver(iterable[i]);
}
return promise;
function resolver(value) {
self.resolve(value).then(function (response) {
if (!called) {
called = true;
handlers.resolve(promise, response);
}
}, function (error) {
if (!called) {
called = true;
handlers.reject(promise, error);
}
});
}
}
return lib;
}
var promiseWorkerTransferable;
var hasRequiredPromiseWorkerTransferable;
function requirePromiseWorkerTransferable () {
if (hasRequiredPromiseWorkerTransferable) return promiseWorkerTransferable;
hasRequiredPromiseWorkerTransferable = 1;
/* istanbul ignore next */
var MyPromise = typeof Promise !== 'undefined' ? Promise : requireLib();
var messageIds = 0;
function onMessage(self, e) {
var message = e.data;
if (!Array.isArray(message) || message.length < 2) {
// Ignore - this message is not for us.
return;
}
var messageId = message[0];
var error = message[1];
var result = message[2];
var callback = self._callbacks[messageId];
if (!callback) {
// Ignore - user might have created multiple PromiseWorkers.
// This message is not for us.
return;
}
delete self._callbacks[messageId];
callback(error, result);
}
function PromiseWorker(worker) {
var self = this;
self._worker = worker;
self._callbacks = {};
worker.addEventListener('message', function (e) {
onMessage(self, e);
});
}
PromiseWorker.prototype.postMessage = function (userMessage, transferList) {
var self = this;
var messageId = messageIds++;
var messageToSend = [messageId, userMessage];
return new MyPromise(function (resolve, reject) {
self._callbacks[messageId] = function (error, result) {
if (error) {
return reject(new Error(error.message));
}
resolve(result);
};
/* istanbul ignore if */
if (typeof self._worker.controller !== 'undefined') {
// service worker, use MessageChannels because e.source is broken in Chrome < 51:
// https://bugs.chromium.org/p/chromium/issues/detail?id=543198
var channel = new MessageChannel();
channel.port1.onmessage = function (e) {
onMessage(self, e);
};
self._worker.controller.postMessage(messageToSend, [channel.port2].concat(transferList));
} else {
// web worker
self._worker.postMessage(messageToSend, transferList);
}
});
};
PromiseWorker.prototype.terminate = function () {
this._worker.terminate();
};
promiseWorkerTransferable = PromiseWorker;
return promiseWorkerTransferable;
}
var promiseWorkerTransferableExports = requirePromiseWorkerTransferable();
var PromiseWorker = /*@__PURE__*/getDefaultExportFromCjs(promiseWorkerTransferableExports);
// @ts-expect-error untyped lib
/**
* Wraps a Regular worker into a `PromiseWorker`
*
* @param worker
* @returns
*/
const getPromiseWorker = (worker) => {
// eslint-disable-next-line @typescript-eslint/no-unsafe-call
return new PromiseWorker(worker);
};
/**
* Returns an interface where methods of the worker can be called by the host site
*
* @example
* // this assumes a vite-like bundler understands the `?worker` import
* import GainMapWorker from '@monogrid/gainmap-js/worker?worker'
* import { getPromiseWorker, getWorkerInterface } from '@monogrid/gainmap-js/worker-interface'
*
* // turn our Worker into a PromiseWorker
* const promiseWorker = getPromiseWorker(new GainMapWorker())
* // get the interface
* const workerInterface = getWorkerInterface(promiseWorker)
*
* @param worker
* @returns
*/
const getWorkerInterface = (worker) => {
return {
compress: (payload) => worker.postMessage({ type: 'compress', payload })
};
};
export { getPromiseWorker, getWorkerInterface };
File diff suppressed because one or more lines are too long
+525
View File
@@ -0,0 +1,525 @@
/**
* @monogrid/gainmap-js v3.4.0
* With ❤️, by MONOGRID <gainmap@monogrid.com>
*/
(function (global, factory) {
typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports) :
typeof define === 'function' && define.amd ? define(['exports'], factory) :
(global = typeof globalThis !== 'undefined' ? globalThis : global || self, factory(global["worker-interface"] = {}));
})(this, (function (exports) { 'use strict';
var commonjsGlobal = typeof globalThis !== 'undefined' ? globalThis : typeof window !== 'undefined' ? window : typeof global !== 'undefined' ? global : typeof self !== 'undefined' ? self : {};
function getDefaultExportFromCjs (x) {
return x && x.__esModule && Object.prototype.hasOwnProperty.call(x, 'default') ? x['default'] : x;
}
var lib$1;
var hasRequiredLib$1;
function requireLib$1 () {
if (hasRequiredLib$1) return lib$1;
hasRequiredLib$1 = 1;
var Mutation = commonjsGlobal.MutationObserver || commonjsGlobal.WebKitMutationObserver;
var scheduleDrain;
if (process.browser) {
if (Mutation) {
var called = 0;
var observer = new Mutation(nextTick);
var element = commonjsGlobal.document.createTextNode('');
observer.observe(element, {
characterData: true
});
scheduleDrain = function () {
element.data = (called = ++called % 2);
};
} else if (!commonjsGlobal.setImmediate && typeof commonjsGlobal.MessageChannel !== 'undefined') {
var channel = new commonjsGlobal.MessageChannel();
channel.port1.onmessage = nextTick;
scheduleDrain = function () {
channel.port2.postMessage(0);
};
} else if ('document' in commonjsGlobal && 'onreadystatechange' in commonjsGlobal.document.createElement('script')) {
scheduleDrain = function () {
// Create a <script> element; its readystatechange event will be fired asynchronously once it is inserted
// into the document. Do so, thus queuing up the task. Remember to clean up once it's been called.
var scriptEl = commonjsGlobal.document.createElement('script');
scriptEl.onreadystatechange = function () {
nextTick();
scriptEl.onreadystatechange = null;
scriptEl.parentNode.removeChild(scriptEl);
scriptEl = null;
};
commonjsGlobal.document.documentElement.appendChild(scriptEl);
};
} else {
scheduleDrain = function () {
setTimeout(nextTick, 0);
};
}
} else {
scheduleDrain = function () {
process.nextTick(nextTick);
};
}
var draining;
var queue = [];
//named nextTick for less confusing stack traces
function nextTick() {
draining = true;
var i, oldQueue;
var len = queue.length;
while (len) {
oldQueue = queue;
queue = [];
i = -1;
while (++i < len) {
oldQueue[i]();
}
len = queue.length;
}
draining = false;
}
lib$1 = immediate;
function immediate(task) {
if (queue.push(task) === 1 && !draining) {
scheduleDrain();
}
}
return lib$1;
}
var lib;
var hasRequiredLib;
function requireLib () {
if (hasRequiredLib) return lib;
hasRequiredLib = 1;
var immediate = requireLib$1();
/* istanbul ignore next */
function INTERNAL() {}
var handlers = {};
var REJECTED = ['REJECTED'];
var FULFILLED = ['FULFILLED'];
var PENDING = ['PENDING'];
/* istanbul ignore else */
if (!process.browser) {
// in which we actually take advantage of JS scoping
var UNHANDLED = ['UNHANDLED'];
}
lib = Promise;
function Promise(resolver) {
if (typeof resolver !== 'function') {
throw new TypeError('resolver must be a function');
}
this.state = PENDING;
this.queue = [];
this.outcome = void 0;
/* istanbul ignore else */
if (!process.browser) {
this.handled = UNHANDLED;
}
if (resolver !== INTERNAL) {
safelyResolveThenable(this, resolver);
}
}
Promise.prototype.finally = function (callback) {
if (typeof callback !== 'function') {
return this;
}
var p = this.constructor;
return this.then(resolve, reject);
function resolve(value) {
function yes () {
return value;
}
return p.resolve(callback()).then(yes);
}
function reject(reason) {
function no () {
throw reason;
}
return p.resolve(callback()).then(no);
}
};
Promise.prototype.catch = function (onRejected) {
return this.then(null, onRejected);
};
Promise.prototype.then = function (onFulfilled, onRejected) {
if (typeof onFulfilled !== 'function' && this.state === FULFILLED ||
typeof onRejected !== 'function' && this.state === REJECTED) {
return this;
}
var promise = new this.constructor(INTERNAL);
/* istanbul ignore else */
if (!process.browser) {
if (this.handled === UNHANDLED) {
this.handled = null;
}
}
if (this.state !== PENDING) {
var resolver = this.state === FULFILLED ? onFulfilled : onRejected;
unwrap(promise, resolver, this.outcome);
} else {
this.queue.push(new QueueItem(promise, onFulfilled, onRejected));
}
return promise;
};
function QueueItem(promise, onFulfilled, onRejected) {
this.promise = promise;
if (typeof onFulfilled === 'function') {
this.onFulfilled = onFulfilled;
this.callFulfilled = this.otherCallFulfilled;
}
if (typeof onRejected === 'function') {
this.onRejected = onRejected;
this.callRejected = this.otherCallRejected;
}
}
QueueItem.prototype.callFulfilled = function (value) {
handlers.resolve(this.promise, value);
};
QueueItem.prototype.otherCallFulfilled = function (value) {
unwrap(this.promise, this.onFulfilled, value);
};
QueueItem.prototype.callRejected = function (value) {
handlers.reject(this.promise, value);
};
QueueItem.prototype.otherCallRejected = function (value) {
unwrap(this.promise, this.onRejected, value);
};
function unwrap(promise, func, value) {
immediate(function () {
var returnValue;
try {
returnValue = func(value);
} catch (e) {
return handlers.reject(promise, e);
}
if (returnValue === promise) {
handlers.reject(promise, new TypeError('Cannot resolve promise with itself'));
} else {
handlers.resolve(promise, returnValue);
}
});
}
handlers.resolve = function (self, value) {
var result = tryCatch(getThen, value);
if (result.status === 'error') {
return handlers.reject(self, result.value);
}
var thenable = result.value;
if (thenable) {
safelyResolveThenable(self, thenable);
} else {
self.state = FULFILLED;
self.outcome = value;
var i = -1;
var len = self.queue.length;
while (++i < len) {
self.queue[i].callFulfilled(value);
}
}
return self;
};
handlers.reject = function (self, error) {
self.state = REJECTED;
self.outcome = error;
/* istanbul ignore else */
if (!process.browser) {
if (self.handled === UNHANDLED) {
immediate(function () {
if (self.handled === UNHANDLED) {
process.emit('unhandledRejection', error, self);
}
});
}
}
var i = -1;
var len = self.queue.length;
while (++i < len) {
self.queue[i].callRejected(error);
}
return self;
};
function getThen(obj) {
// Make sure we only access the accessor once as required by the spec
var then = obj && obj.then;
if (obj && (typeof obj === 'object' || typeof obj === 'function') && typeof then === 'function') {
return function appyThen() {
then.apply(obj, arguments);
};
}
}
function safelyResolveThenable(self, thenable) {
// Either fulfill, reject or reject with error
var called = false;
function onError(value) {
if (called) {
return;
}
called = true;
handlers.reject(self, value);
}
function onSuccess(value) {
if (called) {
return;
}
called = true;
handlers.resolve(self, value);
}
function tryToUnwrap() {
thenable(onSuccess, onError);
}
var result = tryCatch(tryToUnwrap);
if (result.status === 'error') {
onError(result.value);
}
}
function tryCatch(func, value) {
var out = {};
try {
out.value = func(value);
out.status = 'success';
} catch (e) {
out.status = 'error';
out.value = e;
}
return out;
}
Promise.resolve = resolve;
function resolve(value) {
if (value instanceof this) {
return value;
}
return handlers.resolve(new this(INTERNAL), value);
}
Promise.reject = reject;
function reject(reason) {
var promise = new this(INTERNAL);
return handlers.reject(promise, reason);
}
Promise.all = all;
function all(iterable) {
var self = this;
if (Object.prototype.toString.call(iterable) !== '[object Array]') {
return this.reject(new TypeError('must be an array'));
}
var len = iterable.length;
var called = false;
if (!len) {
return this.resolve([]);
}
var values = new Array(len);
var resolved = 0;
var i = -1;
var promise = new this(INTERNAL);
while (++i < len) {
allResolver(iterable[i], i);
}
return promise;
function allResolver(value, i) {
self.resolve(value).then(resolveFromAll, function (error) {
if (!called) {
called = true;
handlers.reject(promise, error);
}
});
function resolveFromAll(outValue) {
values[i] = outValue;
if (++resolved === len && !called) {
called = true;
handlers.resolve(promise, values);
}
}
}
}
Promise.race = race;
function race(iterable) {
var self = this;
if (Object.prototype.toString.call(iterable) !== '[object Array]') {
return this.reject(new TypeError('must be an array'));
}
var len = iterable.length;
var called = false;
if (!len) {
return this.resolve([]);
}
var i = -1;
var promise = new this(INTERNAL);
while (++i < len) {
resolver(iterable[i]);
}
return promise;
function resolver(value) {
self.resolve(value).then(function (response) {
if (!called) {
called = true;
handlers.resolve(promise, response);
}
}, function (error) {
if (!called) {
called = true;
handlers.reject(promise, error);
}
});
}
}
return lib;
}
var promiseWorkerTransferable;
var hasRequiredPromiseWorkerTransferable;
function requirePromiseWorkerTransferable () {
if (hasRequiredPromiseWorkerTransferable) return promiseWorkerTransferable;
hasRequiredPromiseWorkerTransferable = 1;
/* istanbul ignore next */
var MyPromise = typeof Promise !== 'undefined' ? Promise : requireLib();
var messageIds = 0;
function onMessage(self, e) {
var message = e.data;
if (!Array.isArray(message) || message.length < 2) {
// Ignore - this message is not for us.
return;
}
var messageId = message[0];
var error = message[1];
var result = message[2];
var callback = self._callbacks[messageId];
if (!callback) {
// Ignore - user might have created multiple PromiseWorkers.
// This message is not for us.
return;
}
delete self._callbacks[messageId];
callback(error, result);
}
function PromiseWorker(worker) {
var self = this;
self._worker = worker;
self._callbacks = {};
worker.addEventListener('message', function (e) {
onMessage(self, e);
});
}
PromiseWorker.prototype.postMessage = function (userMessage, transferList) {
var self = this;
var messageId = messageIds++;
var messageToSend = [messageId, userMessage];
return new MyPromise(function (resolve, reject) {
self._callbacks[messageId] = function (error, result) {
if (error) {
return reject(new Error(error.message));
}
resolve(result);
};
/* istanbul ignore if */
if (typeof self._worker.controller !== 'undefined') {
// service worker, use MessageChannels because e.source is broken in Chrome < 51:
// https://bugs.chromium.org/p/chromium/issues/detail?id=543198
var channel = new MessageChannel();
channel.port1.onmessage = function (e) {
onMessage(self, e);
};
self._worker.controller.postMessage(messageToSend, [channel.port2].concat(transferList));
} else {
// web worker
self._worker.postMessage(messageToSend, transferList);
}
});
};
PromiseWorker.prototype.terminate = function () {
this._worker.terminate();
};
promiseWorkerTransferable = PromiseWorker;
return promiseWorkerTransferable;
}
var promiseWorkerTransferableExports = requirePromiseWorkerTransferable();
var PromiseWorker = /*@__PURE__*/getDefaultExportFromCjs(promiseWorkerTransferableExports);
// @ts-expect-error untyped lib
/**
* Wraps a Regular worker into a `PromiseWorker`
*
* @param worker
* @returns
*/
const getPromiseWorker = (worker) => {
// eslint-disable-next-line @typescript-eslint/no-unsafe-call
return new PromiseWorker(worker);
};
/**
* Returns an interface where methods of the worker can be called by the host site
*
* @example
* // this assumes a vite-like bundler understands the `?worker` import
* import GainMapWorker from '@monogrid/gainmap-js/worker?worker'
* import { getPromiseWorker, getWorkerInterface } from '@monogrid/gainmap-js/worker-interface'
*
* // turn our Worker into a PromiseWorker
* const promiseWorker = getPromiseWorker(new GainMapWorker())
* // get the interface
* const workerInterface = getWorkerInterface(promiseWorker)
*
* @param worker
* @returns
*/
const getWorkerInterface = (worker) => {
return {
compress: (payload) => worker.postMessage({ type: 'compress', payload })
};
};
exports.getPromiseWorker = getPromiseWorker;
exports.getWorkerInterface = getWorkerInterface;
}));
File diff suppressed because one or more lines are too long
+24
View File
@@ -0,0 +1,24 @@
import { CompressedImage, type CompressParameters } from './encode/types';
export type WorkerInterface = {
compress: {
request: {
type: 'compress';
payload: CompressParameters;
};
result: Awaited<CompressedImage> & {
source: Uint8ClampedArray;
};
};
};
/**
* Transferable
*/
export type Transferable = ArrayBufferLike | ImageBitmap;
export type WorkerInterfaceImplementation = {
[k in keyof WorkerInterface]: (payload: WorkerInterface[k]['request']['payload']) => Promise<WorkerInterface[k]['result']>;
};
export type WorkerRequest = WorkerInterface[keyof WorkerInterface]['request'];
export type WithTransferListFunction = <T>(payload: T, transferList: Transferable[]) => T;
export type PromiseWorkerType = {
postMessage: (message: any, transferables?: Transferable[]) => Promise<any>;
};
+1
View File
@@ -0,0 +1 @@
export {};
+142
View File
@@ -0,0 +1,142 @@
/**
* @monogrid/gainmap-js v3.4.0
* With ❤️, by MONOGRID <gainmap@monogrid.com>
*/
import { g as getDefaultExportFromCjs } from './_commonjsHelpers-v3qvwW2J.js';
import { c as compress } from './compress-C-X439Jm.js';
var isPromise = {exports: {}};
var hasRequiredIsPromise;
function requireIsPromise () {
if (hasRequiredIsPromise) return isPromise.exports;
hasRequiredIsPromise = 1;
isPromise.exports = isPromise$1;
isPromise.exports.default = isPromise$1;
function isPromise$1(obj) {
return !!obj && (typeof obj === 'object' || typeof obj === 'function') && typeof obj.then === 'function';
}
return isPromise.exports;
}
var register;
var hasRequiredRegister;
function requireRegister () {
if (hasRequiredRegister) return register;
hasRequiredRegister = 1;
var isPromise = requireIsPromise();
function registerPromiseWorker(callback) {
function postOutgoingMessage(e, messageId, error, result) {
function postMessage(msg, transferList) {
/* istanbul ignore if */
if (typeof self.postMessage !== 'function') { // service worker
e.ports[0].postMessage(msg, transferList);
} else { // web worker
self.postMessage(msg, transferList);
}
}
if (error) {
/* istanbul ignore else */
if (typeof console !== 'undefined' && 'error' in console) {
// This is to make errors easier to debug. I think it's important
// enough to just leave here without giving the user an option
// to silence it.
console.error('Worker caught an error:', error);
}
postMessage([messageId, {
message: error.message
}]);
} else {
if (result instanceof MessageWithTransferList) {
postMessage([messageId, null, result.message], result.transferList);
} else {
postMessage([messageId, null, result]);
}
}
}
function tryCatchFunc(callback, message) {
try {
return {res: callback(message, withTransferList)};
} catch (e) {
return {err: e};
}
}
function withTransferList(resMessage, transferList) {
return new MessageWithTransferList(resMessage, transferList);
}
function handleIncomingMessage(e, callback, messageId, message) {
var result = tryCatchFunc(callback, message);
if (result.err) {
postOutgoingMessage(e, messageId, result.err);
} else if (!isPromise(result.res)) {
postOutgoingMessage(e, messageId, null, result.res);
} else {
result.res.then(function (finalResult) {
postOutgoingMessage(e, messageId, null, finalResult);
}, function (finalError) {
postOutgoingMessage(e, messageId, finalError);
});
}
}
function onIncomingMessage(e) {
var payload = e.data;
if (!Array.isArray(payload) || payload.length !== 2) {
// message doens't match communication format; ignore
return;
}
var messageId = payload[0];
var message = payload[1];
if (typeof callback !== 'function') {
postOutgoingMessage(e, messageId, new Error(
'Please pass a function into register().'));
} else {
handleIncomingMessage(e, callback, messageId, message);
}
}
function MessageWithTransferList(message, transferList) {
this.message = message;
this.transferList = transferList;
}
self.addEventListener('message', onIncomingMessage);
}
register = registerPromiseWorker;
return register;
}
var registerExports = requireRegister();
var registerPromiseWorker = /*@__PURE__*/getDefaultExportFromCjs(registerExports);
// @ts-expect-error untyped lib
const _compress = async (message, withTransferList) => {
const result = await compress(message.payload);
return withTransferList({
...result,
source: message.payload.source instanceof ImageData ? message.payload.source.data : new Uint8ClampedArray(message.payload.source)
}, [result.data.buffer, message.payload.source instanceof ImageData ? message.payload.source.data.buffer : message.payload.source.buffer]);
};
// eslint-disable-next-line @typescript-eslint/no-unsafe-call
registerPromiseWorker(async (message, withTransferList) => {
switch (message.type) {
// case 'encode-gainmap-buffers':
// return encodeGainmapBuffers(message, withTransferList)
case 'compress':
return _compress(message, withTransferList);
}
});
+5
View File
@@ -0,0 +1,5 @@
/**
* @monogrid/gainmap-js v3.4.0
* With ❤️, by MONOGRID <gainmap@monogrid.com>
*/
import{g as e}from"./_commonjsHelpers-CDmClQiF.js";import{c as r}from"./compress-DY-CSunh.js";var n,t,o,s={exports:{}};function a(){if(n)return s.exports;function e(e){return!!e&&("object"==typeof e||"function"==typeof e)&&"function"==typeof e.then}return n=1,s.exports=e,s.exports.default=e,s.exports}var f=function(){if(o)return t;o=1;var e=a();return t=function(r){function n(e,r,n,t){function o(r,n){"function"!=typeof self.postMessage?e.ports[0].postMessage(r,n):self.postMessage(r,n)}n?("undefined"!=typeof console&&"error"in console&&console.error("Worker caught an error:",n),o([r,{message:n.message}])):t instanceof s?o([r,null,t.message],t.transferList):o([r,null,t])}function t(e,r){return new s(e,r)}function o(r,o,s,a){var f=function(e,r){try{return{res:e(r,t)}}catch(e){return{err:e}}}(o,a);f.err?n(r,s,f.err):e(f.res)?f.res.then(function(e){n(r,s,null,e)},function(e){n(r,s,e)}):n(r,s,null,f.res)}function s(e,r){this.message=e,this.transferList=r}self.addEventListener("message",function(e){var t=e.data;if(Array.isArray(t)&&2===t.length){var s=t[0],a=t[1];"function"!=typeof r?n(e,s,new Error("Please pass a function into register().")):o(e,r,s,a)}})}}();e(f)(async(e,n)=>{if("compress"===e.type)return(async(e,n)=>{const t=await r(e.payload);return n({...t,source:e.payload.source instanceof ImageData?e.payload.source.data:new Uint8ClampedArray(e.payload.source)},[t.data.buffer,e.payload.source instanceof ImageData?e.payload.source.data.buffer:e.payload.source.buffer])})(e,n)});
+236
View File
@@ -0,0 +1,236 @@
/**
* @monogrid/gainmap-js v3.4.0
* With ❤️, by MONOGRID <gainmap@monogrid.com>
*/
(function (factory) {
typeof define === 'function' && define.amd ? define(factory) :
factory();
})((function () { 'use strict';
function getDefaultExportFromCjs (x) {
return x && x.__esModule && Object.prototype.hasOwnProperty.call(x, 'default') ? x['default'] : x;
}
var isPromise = {exports: {}};
var hasRequiredIsPromise;
function requireIsPromise () {
if (hasRequiredIsPromise) return isPromise.exports;
hasRequiredIsPromise = 1;
isPromise.exports = isPromise$1;
isPromise.exports.default = isPromise$1;
function isPromise$1(obj) {
return !!obj && (typeof obj === 'object' || typeof obj === 'function') && typeof obj.then === 'function';
}
return isPromise.exports;
}
var register;
var hasRequiredRegister;
function requireRegister () {
if (hasRequiredRegister) return register;
hasRequiredRegister = 1;
var isPromise = requireIsPromise();
function registerPromiseWorker(callback) {
function postOutgoingMessage(e, messageId, error, result) {
function postMessage(msg, transferList) {
/* istanbul ignore if */
if (typeof self.postMessage !== 'function') { // service worker
e.ports[0].postMessage(msg, transferList);
} else { // web worker
self.postMessage(msg, transferList);
}
}
if (error) {
/* istanbul ignore else */
if (typeof console !== 'undefined' && 'error' in console) {
// This is to make errors easier to debug. I think it's important
// enough to just leave here without giving the user an option
// to silence it.
console.error('Worker caught an error:', error);
}
postMessage([messageId, {
message: error.message
}]);
} else {
if (result instanceof MessageWithTransferList) {
postMessage([messageId, null, result.message], result.transferList);
} else {
postMessage([messageId, null, result]);
}
}
}
function tryCatchFunc(callback, message) {
try {
return {res: callback(message, withTransferList)};
} catch (e) {
return {err: e};
}
}
function withTransferList(resMessage, transferList) {
return new MessageWithTransferList(resMessage, transferList);
}
function handleIncomingMessage(e, callback, messageId, message) {
var result = tryCatchFunc(callback, message);
if (result.err) {
postOutgoingMessage(e, messageId, result.err);
} else if (!isPromise(result.res)) {
postOutgoingMessage(e, messageId, null, result.res);
} else {
result.res.then(function (finalResult) {
postOutgoingMessage(e, messageId, null, finalResult);
}, function (finalError) {
postOutgoingMessage(e, messageId, finalError);
});
}
}
function onIncomingMessage(e) {
var payload = e.data;
if (!Array.isArray(payload) || payload.length !== 2) {
// message doens't match communication format; ignore
return;
}
var messageId = payload[0];
var message = payload[1];
if (typeof callback !== 'function') {
postOutgoingMessage(e, messageId, new Error(
'Please pass a function into register().'));
} else {
handleIncomingMessage(e, callback, messageId, message);
}
}
function MessageWithTransferList(message, transferList) {
this.message = message;
this.transferList = transferList;
}
self.addEventListener('message', onIncomingMessage);
}
register = registerPromiseWorker;
return register;
}
var registerExports = requireRegister();
var registerPromiseWorker = /*@__PURE__*/getDefaultExportFromCjs(registerExports);
/**
* Used internally
*
* @internal
* @param canvas
* @param mimeType
* @param quality
* @returns
*/
const canvasToBlob = async (canvas, mimeType, quality) => {
if (typeof OffscreenCanvas !== 'undefined' && canvas instanceof OffscreenCanvas) {
return canvas.convertToBlob({ type: mimeType, quality: quality });
}
else if (canvas instanceof HTMLCanvasElement) {
return new Promise((resolve, reject) => {
canvas.toBlob((res) => {
if (res)
resolve(res);
else
reject(new Error('Failed to convert canvas to blob'));
}, mimeType, quality);
});
}
/* istanbul ignore next
as long as this function is not exported this is only here
to satisfy TS strict mode internally
*/
throw new Error('Unsupported canvas element');
};
/**
* Converts a RAW RGBA image buffer into the provided `mimeType` using the provided `quality`
*
* @category Compression
* @group Compression
* @param params
* @throws {Error} if the browser does not support [createImageBitmap](https://caniuse.com/createimagebitmap)
* @throws {Error} if the provided source image cannot be decoded
* @throws {Error} if the function fails to create a canvas context
*/
const compress = async (params) => {
if (typeof createImageBitmap === 'undefined')
throw new Error('createImageBitmap() not supported.');
const { source, mimeType, quality, flipY } = params;
let imageBitmapSource;
if ((source instanceof Uint8Array || source instanceof Uint8ClampedArray) && 'sourceMimeType' in params) {
imageBitmapSource = new Blob([source], { type: params.sourceMimeType });
}
else if (source instanceof ImageData) {
imageBitmapSource = source;
}
else {
throw new Error('Invalid source image');
}
const img = await createImageBitmap(imageBitmapSource);
const width = img.width;
const height = img.height;
let canvas;
if (typeof OffscreenCanvas !== 'undefined') {
canvas = new OffscreenCanvas(width, height);
}
else {
canvas = document.createElement('canvas');
canvas.width = width;
canvas.height = height;
}
// apparently we do need to assert the type here for npx tsc to be happy
// eslint-disable-next-line @typescript-eslint/no-unnecessary-type-assertion
const ctx = canvas.getContext('2d');
if (!ctx)
throw new Error('Failed to create canvas Context');
// flip Y
if (flipY === true) {
ctx.translate(0, height);
ctx.scale(1, -1);
}
ctx.drawImage(img, 0, 0, width, height);
const blob = await canvasToBlob(canvas, mimeType, quality || 0.9);
const data = new Uint8Array(await blob.arrayBuffer());
return {
data,
mimeType,
width,
height
};
};
// @ts-expect-error untyped lib
const _compress = async (message, withTransferList) => {
const result = await compress(message.payload);
return withTransferList({
...result,
source: message.payload.source instanceof ImageData ? message.payload.source.data : new Uint8ClampedArray(message.payload.source)
}, [result.data.buffer, message.payload.source instanceof ImageData ? message.payload.source.data.buffer : message.payload.source.buffer]);
};
// eslint-disable-next-line @typescript-eslint/no-unsafe-call
registerPromiseWorker(async (message, withTransferList) => {
switch (message.type) {
// case 'encode-gainmap-buffers':
// return encodeGainmapBuffers(message, withTransferList)
case 'compress':
return _compress(message, withTransferList);
}
});
}));
+5
View File
@@ -0,0 +1,5 @@
/**
* @monogrid/gainmap-js v3.4.0
* With ❤️, by MONOGRID <gainmap@monogrid.com>
*/
!function(e){"function"==typeof define&&define.amd?define(e):e()}(function(){"use strict";function e(e){return e&&e.__esModule&&Object.prototype.hasOwnProperty.call(e,"default")?e.default:e}var t,n,r,a={exports:{}};function o(){if(t)return a.exports;function e(e){return!!e&&("object"==typeof e||"function"==typeof e)&&"function"==typeof e.then}return t=1,a.exports=e,a.exports.default=e,a.exports}var s=function(){if(r)return n;r=1;var e=o();return n=function(t){function n(e,t,n,r){function a(t,n){"function"!=typeof self.postMessage?e.ports[0].postMessage(t,n):self.postMessage(t,n)}n?("undefined"!=typeof console&&"error"in console&&console.error("Worker caught an error:",n),a([t,{message:n.message}])):r instanceof o?a([t,null,r.message],r.transferList):a([t,null,r])}function r(e,t){return new o(e,t)}function a(t,a,o,s){var i=function(e,t){try{return{res:e(t,r)}}catch(e){return{err:e}}}(a,s);i.err?n(t,o,i.err):e(i.res)?i.res.then(function(e){n(t,o,null,e)},function(e){n(t,o,e)}):n(t,o,null,i.res)}function o(e,t){this.message=e,this.transferList=t}self.addEventListener("message",function(e){var r=e.data;if(Array.isArray(r)&&2===r.length){var o=r[0],s=r[1];"function"!=typeof t?n(e,o,new Error("Please pass a function into register().")):a(e,t,o,s)}})}}();const i=async e=>{if("undefined"==typeof createImageBitmap)throw new Error("createImageBitmap() not supported.");const{source:t,mimeType:n,quality:r,flipY:a}=e;let o;if((t instanceof Uint8Array||t instanceof Uint8ClampedArray)&&"sourceMimeType"in e)o=new Blob([t],{type:e.sourceMimeType});else{if(!(t instanceof ImageData))throw new Error("Invalid source image");o=t}const s=await createImageBitmap(o),i=s.width,f=s.height;let c;"undefined"!=typeof OffscreenCanvas?c=new OffscreenCanvas(i,f):(c=document.createElement("canvas"),c.width=i,c.height=f);const u=c.getContext("2d");if(!u)throw new Error("Failed to create canvas Context");!0===a&&(u.translate(0,f),u.scale(1,-1)),u.drawImage(s,0,0,i,f);const l=await(async(e,t,n)=>{if("undefined"!=typeof OffscreenCanvas&&e instanceof OffscreenCanvas)return e.convertToBlob({type:t,quality:n});if(e instanceof HTMLCanvasElement)return new Promise((r,a)=>{e.toBlob(e=>{e?r(e):a(new Error("Failed to convert canvas to blob"))},t,n)});throw new Error("Unsupported canvas element")})(c,n,r||.9);return{data:new Uint8Array(await l.arrayBuffer()),mimeType:n,width:i,height:f}};e(s)(async(e,t)=>{if("compress"===e.type)return(async(e,t)=>{const n=await i(e.payload);return t({...n,source:e.payload.source instanceof ImageData?e.payload.source.data:new Uint8ClampedArray(e.payload.source)},[n.data.buffer,e.payload.source instanceof ImageData?e.payload.source.data.buffer:e.payload.source.buffer])})(e,t)})});
+167
View File
@@ -0,0 +1,167 @@
{
"name": "@monogrid/gainmap-js",
"version": "3.4.0",
"description": "A Javascript (TypeScript) Port of Adobe Gainmap Technology for storing HDR Images using an SDR Image + a gain map",
"keywords": [
"hdr",
"gain map",
"gainmap",
"three",
"threejs"
],
"homepage": "https://github.com/MONOGRID/gainmap-js#readme",
"bugs": {
"url": "https://github.com/MONOGRID/gainmap-js/issues"
},
"repository": {
"type": "git",
"url": "git+ssh://git@github.com/MONOGRID/gainmap-js.git"
},
"license": "MIT",
"author": "MONOGRID <gainmap@monogrid.com>",
"sideEffects": false,
"type": "module",
"exports": {
".": {
"types": "./dist/decode.d.ts",
"import": "./dist/decode.js",
"default": "./dist/decode.umd.cjs"
},
"./webgpu": {
"types": "./dist/decode/webgpu/index.d.ts",
"import": "./dist/decode.webgpu.js",
"default": "./dist/decode.webgpu.umd.cjs"
},
"./encode": {
"types": "./dist/encode.d.ts",
"import": "./dist/encode.js",
"default": "./dist/encode.umd.cjs"
},
"./libultrahdr": {
"types": "./dist/libultrahdr.d.ts",
"import": "./dist/libultrahdr.js",
"default": "./dist/libultrahdr.umd.cjs"
},
"./worker": {
"types": "./dist/worker.d.ts",
"import": "./dist/worker.js",
"default": "./dist/worker.umd.cjs"
},
"./worker-interface": {
"types": "./dist/worker-interface.d.ts",
"import": "./dist/worker-interface.js",
"default": "./dist/worker-interface.umd.cjs"
}
},
"main": "dist/decode.umd.cjs",
"module": "dist/decode.js",
"types": "dist/decode.d.ts",
"typesVersions": {
"*": {
".": [
"./dist/decode.d.ts"
],
"decode/webgpu": [
"./dist/decode/webgpu/index.d.ts"
],
"decode/webgl": [
"./dist/decode/webgl/index.d.ts"
],
"encode": [
"./dist/encode.d.ts"
],
"libultrahdr": [
"./dist/libultrahdr.d.ts"
],
"worker": [
"./dist/worker.d.ts"
],
"worker-interface": [
"./dist/worker-interface.d.ts"
]
}
},
"scripts": {
"build": "rollup -c",
"check": "concurrently -c auto npm:check:*",
"check:eslint-examples": "eslint \"examples/**/*.{ts,html}\"",
"check:eslint-src": "eslint \"src/**/*.ts\"",
"check:eslint-tests": "eslint \"tests/**/*.ts\"",
"check:typecheck-examples": "tsc -p examples",
"check:typecheck-src": "tsc -p src",
"check:typecheck-tests": "tsc -p tests",
"ci:check": "concurrently npm:ci:check:*",
"ci:check:eslint-examples": "eslint --format json --output-file reports/eslint-examples.json \"examples/**/*.{ts,html}\"",
"ci:check:eslint-src": "eslint --format json --output-file reports/eslint-src.json \"src/**/*.ts\"",
"ci:check:eslint-tests": "eslint --format json --output-file reports/eslint-tests.json \"tests/**/*.ts\"",
"ci:check:typecheck-examples": "tsc --pretty false -p examples > reports/typecheck-examples.log",
"ci:check:typecheck-src": "tsc --pretty false -p src > reports/typecheck.log",
"ci:check:typecheck-tests": "tsc --pretty false -p tests > reports/typecheck-tests.log",
"dev": "concurrently -n rollup,servez -c magenta,green \"rollup -c -w\" \"servez\"",
"prepack": "npm run build",
"start": "concurrently -n rollup,servez -c magenta,green \"rollup -c -w\" \"servez\"",
"test": "nyc --reporter=text --reporter=lcov playwright test",
"test:codegen": "playwright codegen http://localhost:8080",
"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.56.1-jammy /bin/bash",
"test:startserver": "rollup -c && servez"
},
"config": {
"commitizen": {
"path": "./node_modules/cz-conventional-changelog"
}
},
"browserslist": [
"> 1%, not dead, not ie 11, not op_mini all"
],
"dependencies": {
"promise-worker-transferable": "^1.0.4"
},
"devDependencies": {
"@eslint/eslintrc": "^3.3.1",
"@playwright/test": "^1.56.1",
"@rollup/plugin-commonjs": "^29.0.0",
"@rollup/plugin-json": "^6.1.0",
"@rollup/plugin-node-resolve": "^16.0.3",
"@rollup/plugin-terser": "^0.4.4",
"@rollup/plugin-typescript": "^12.3.0",
"@semantic-release/changelog": "^6.0.3",
"@semantic-release/commit-analyzer": "^13.0.1",
"@semantic-release/git": "github:semantic-release/git",
"@semantic-release/github": "^12.0.0",
"@semantic-release/npm": "^13.1.1",
"@semantic-release/release-notes-generator": "^14.1.0",
"@types/node": "^22.18.12",
"@types/three": "^0.181.0",
"concurrently": "^9.2.1",
"conventional-changelog-conventionalcommits": "^9.1.0",
"cz-conventional-changelog": "^3.3.0",
"eslint": "^9.38.0",
"eslint-config-mdcs": "^5.0.0",
"eslint-plugin-html": "^8.1.3",
"eslint-plugin-simple-import-sort": "^12.1.1",
"eslint-plugin-unused-imports": "^4.3.0",
"globals": "^16.4.0",
"neostandard": "^0.12.2",
"nyc": "^17.1.0",
"rollup": "^4.52.5",
"rollup-plugin-copy": "^3.5.0",
"rollup-plugin-delete": "^3.0.1",
"rollup-plugin-istanbul": "^5.0.0",
"rollup-plugin-license": "^3.6.0",
"semantic-release": "^25.0.1",
"servez": "^2.3.2",
"sharp": "^0.34.4",
"three": "^0.181.1",
"typedoc": "^0.28.14",
"typedoc-github-wiki-theme": "^2.1.0",
"typedoc-plugin-markdown": "^4.9.0",
"typescript": "~5.9.0"
},
"peerDependencies": {
"three": ">= 0.159.0"
},
"publishConfig": {
"access": "public",
"registry": "https://registry.npmjs.org/"
}
}