clone

a photo toolkit for your phone that never uploads: fuji film looks for raw files, social crops, error level analysis

Fuji tool: develop RAF with LibRaw wasm and apply film simulation LUTs

Dami committed a3b0c90
commit
a3b0c9010ff505c4daf898f110084e9baecf37ee
parent
d0461c2d4dcb2798d65f6f9cf553ae156650924a
author
Dami <dami.etoile@gmail.com>
35 files changed, +818 -55
ALICENSES/LGPL-2.1.txt458++++++++++++++++++++++++++++++
ALICENSES/LIBRAW-COPYRIGHT.txt27++
MNOTICE.md6+
MREADME.md4+-
Aassets/luts/acros-green.pngbin
Aassets/luts/acros-red.pngbin
Aassets/luts/acros-yellow.pngbin
Aassets/luts/acros.pngbin
Aassets/luts/astia.pngbin
Aassets/luts/classic_chrome.pngbin
Aassets/luts/eterna.pngbin
Aassets/luts/mono-green.pngbin
Aassets/luts/mono-red.pngbin
Aassets/luts/mono-yellow.pngbin
Aassets/luts/mono.pngbin
Aassets/luts/neutral-dr100.pngbin
Aassets/luts/neutral-dr200.pngbin
Aassets/luts/neutral-dr400.pngbin
Aassets/luts/pro_neg_high.pngbin
Aassets/luts/pro_neg_std.pngbin
Aassets/luts/provia.pngbin
Aassets/luts/sepia.pngbin
Aassets/luts/velvia.pngbin
Aassets/raw.mjs2+
Aassets/raw.wasmbin
Mfuji.html28+-
Mfuji.mjs117++++++--
Mindex.html5+-
Anative/raw/build.sh14+
Anative/raw/wrapper.cpp39+++
Arawworker.mjs77+++++
Ascripts/neutral-lut.py70+++++
Mscripts/package.py9+-
Mscripts/serve.py6+-
Mstyle.css11+-
A LICENSES/LGPL-2.1.txt +458 -0
@@ -0,0 +1,458 @@
1+ GNU LESSER GENERAL PUBLIC LICENSE2+ Version 2.1, February 19993+4+ Copyright (C) 1991, 1999 Free Software Foundation, Inc.5+ 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA6+ Everyone is permitted to copy and distribute verbatim copies7+ of this license document, but changing it is not allowed.8+9+[This is the first released version of the Lesser GPL. It also counts10+ as the successor of the GNU Library Public License, version 2, hence11+ the version number 2.1.]12+13+ Preamble14+15+ The licenses for most software are designed to take away your16+freedom to share and change it. By contrast, the GNU General Public17+Licenses are intended to guarantee your freedom to share and change18+free software--to make sure the software is free for all its users.19+20+ This license, the Lesser General Public License, applies to some21+specially designated software packages--typically libraries--of the22+Free Software Foundation and other authors who decide to use it. You23+can use it too, but we suggest you first think carefully about whether24+this license or the ordinary General Public License is the better25+strategy to use in any particular case, based on the explanations below.26+27+ When we speak of free software, we are referring to freedom of use,28+not price. Our General Public Licenses are designed to make sure that29+you have the freedom to distribute copies of free software (and charge30+for this service if you wish); that you receive source code or can get31+it if you want it; that you can change the software and use pieces of32+it in new free programs; and that you are informed that you can do33+these things.34+35+ To protect your rights, we need to make restrictions that forbid36+distributors to deny you these rights or to ask you to surrender these37+rights. These restrictions translate to certain responsibilities for38+you if you distribute copies of the library or if you modify it.39+40+ For example, if you distribute copies of the library, whether gratis41+or for a fee, you must give the recipients all the rights that we gave42+you. You must make sure that they, too, receive or can get the source43+code. If you link other code with the library, you must provide44+complete object files to the recipients, so that they can relink them45+with the library after making changes to the library and recompiling46+it. And you must show them these terms so they know their rights.47+48+ We protect your rights with a two-step method: (1) we copyright the49+library, and (2) we offer you this license, which gives you legal50+permission to copy, distribute and/or modify the library.51+52+ To protect each distributor, we want to make it very clear that53+there is no warranty for the free library. Also, if the library is54+modified by someone else and passed on, the recipients should know55+that what they have is not the original version, so that the original56+author's reputation will not be affected by problems that might be57+introduced by others.58+ 59+ Finally, software patents pose a constant threat to the existence of60+any free program. We wish to make sure that a company cannot61+effectively restrict the users of a free program by obtaining a62+restrictive license from a patent holder. Therefore, we insist that63+any patent license obtained for a version of the library must be64+consistent with the full freedom of use specified in this license.65+66+ Most GNU software, including some libraries, is covered by the67+ordinary GNU General Public License. This license, the GNU Lesser68+General Public License, applies to certain designated libraries, and69+is quite different from the ordinary General Public License. We use70+this license for certain libraries in order to permit linking those71+libraries into non-free programs.72+73+ When a program is linked with a library, whether statically or using74+a shared library, the combination of the two is legally speaking a75+combined work, a derivative of the original library. The ordinary76+General Public License therefore permits such linking only if the77+entire combination fits its criteria of freedom. The Lesser General78+Public License permits more lax criteria for linking other code with79+the library.80+81+ We call this license the "Lesser" General Public License because it82+does Less to protect the user's freedom than the ordinary General83+Public License. It also provides other free software developers Less84+of an advantage over competing non-free programs. These disadvantages85+are the reason we use the ordinary General Public License for many86+libraries. However, the Lesser license provides advantages in certain87+special circumstances.88+89+ For example, on rare occasions, there may be a special need to90+encourage the widest possible use of a certain library, so that it becomes91+a de-facto standard. To achieve this, non-free programs must be92+allowed to use the library. A more frequent case is that a free93+library does the same job as widely used non-free libraries. In this94+case, there is little to gain by limiting the free library to free95+software only, so we use the Lesser General Public License.96+97+ In other cases, permission to use a particular library in non-free98+programs enables a greater number of people to use a large body of99+free software. For example, permission to use the GNU C Library in100+non-free programs enables many more people to use the whole GNU101+operating system, as well as its variant, the GNU/Linux operating102+system.103+104+ Although the Lesser General Public License is Less protective of the105+users' freedom, it does ensure that the user of a program that is106+linked with the Library has the freedom and the wherewithal to run107+that program using a modified version of the Library.108+109+ The precise terms and conditions for copying, distribution and110+modification follow. Pay close attention to the difference between a111+"work based on the library" and a "work that uses the library". The112+former contains code derived from the library, whereas the latter must113+be combined with the library in order to run.114+ 115+ GNU LESSER GENERAL PUBLIC LICENSE116+ TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION117+118+ 0. This License Agreement applies to any software library or other119+program which contains a notice placed by the copyright holder or120+other authorized party saying it may be distributed under the terms of121+this Lesser General Public License (also called "this License").122+Each licensee is addressed as "you".123+124+ A "library" means a collection of software functions and/or data125+prepared so as to be conveniently linked with application programs126+(which use some of those functions and data) to form executables.127+128+ The "Library", below, refers to any such software library or work129+which has been distributed under these terms. A "work based on the130+Library" means either the Library or any derivative work under131+copyright law: that is to say, a work containing the Library or a132+portion of it, either verbatim or with modifications and/or translated133+straightforwardly into another language. (Hereinafter, translation is134+included without limitation in the term "modification".)135+136+ "Source code" for a work means the preferred form of the work for137+making modifications to it. For a library, complete source code means138+all the source code for all modules it contains, plus any associated139+interface definition files, plus the scripts used to control compilation140+and installation of the library.141+142+ Activities other than copying, distribution and modification are not143+covered by this License; they are outside its scope. The act of144+running a program using the Library is not restricted, and output from145+such a program is covered only if its contents constitute a work based146+on the Library (independent of the use of the Library in a tool for147+writing it). Whether that is true depends on what the Library does148+and what the program that uses the Library does.149+ 150+ 1. You may copy and distribute verbatim copies of the Library's151+complete source code as you receive it, in any medium, provided that152+you conspicuously and appropriately publish on each copy an153+appropriate copyright notice and disclaimer of warranty; keep intact154+all the notices that refer to this License and to the absence of any155+warranty; and distribute a copy of this License along with the156+Library.157+158+ You may charge a fee for the physical act of transferring a copy,159+and you may at your option offer warranty protection in exchange for a160+fee.161+ 162+ 2. You may modify your copy or copies of the Library or any portion163+of it, thus forming a work based on the Library, and copy and164+distribute such modifications or work under the terms of Section 1165+above, provided that you also meet all of these conditions:166+167+ a) The modified work must itself be a software library.168+169+ b) You must cause the files modified to carry prominent notices170+ stating that you changed the files and the date of any change.171+172+ c) You must cause the whole of the work to be licensed at no173+ charge to all third parties under the terms of this License.174+175+ d) If a facility in the modified Library refers to a function or a176+ table of data to be supplied by an application program that uses177+ the facility, other than as an argument passed when the facility178+ is invoked, then you must make a good faith effort to ensure that,179+ in the event an application does not supply such function or180+ table, the facility still operates, and performs whatever part of181+ its purpose remains meaningful.182+183+ (For example, a function in a library to compute square roots has184+ a purpose that is entirely well-defined independent of the185+ application. Therefore, Subsection 2d requires that any186+ application-supplied function or table used by this function must187+ be optional: if the application does not supply it, the square188+ root function must still compute square roots.)189+190+These requirements apply to the modified work as a whole. If191+identifiable sections of that work are not derived from the Library,192+and can be reasonably considered independent and separate works in193+themselves, then this License, and its terms, do not apply to those194+sections when you distribute them as separate works. But when you195+distribute the same sections as part of a whole which is a work based196+on the Library, the distribution of the whole must be on the terms of197+this License, whose permissions for other licensees extend to the198+entire whole, and thus to each and every part regardless of who wrote199+it.200+201+Thus, it is not the intent of this section to claim rights or contest202+your rights to work written entirely by you; rather, the intent is to203+exercise the right to control the distribution of derivative or204+collective works based on the Library.205+206+In addition, mere aggregation of another work not based on the Library207+with the Library (or with a work based on the Library) on a volume of208+a storage or distribution medium does not bring the other work under209+the scope of this License.210+211+ 3. You may opt to apply the terms of the ordinary GNU General Public212+License instead of this License to a given copy of the Library. To do213+this, you must alter all the notices that refer to this License, so214+that they refer to the ordinary GNU General Public License, version 2,215+instead of to this License. (If a newer version than version 2 of the216+ordinary GNU General Public License has appeared, then you can specify217+that version instead if you wish.) Do not make any other change in218+these notices.219+ 220+ Once this change is made in a given copy, it is irreversible for221+that copy, so the ordinary GNU General Public License applies to all222+subsequent copies and derivative works made from that copy.223+224+ This option is useful when you wish to copy part of the code of225+the Library into a program that is not a library.226+227+ 4. You may copy and distribute the Library (or a portion or228+derivative of it, under Section 2) in object code or executable form229+under the terms of Sections 1 and 2 above provided that you accompany230+it with the complete corresponding machine-readable source code, which231+must be distributed under the terms of Sections 1 and 2 above on a232+medium customarily used for software interchange.233+234+ If distribution of object code is made by offering access to copy235+from a designated place, then offering equivalent access to copy the236+source code from the same place satisfies the requirement to237+distribute the source code, even though third parties are not238+compelled to copy the source along with the object code.239+240+ 5. A program that contains no derivative of any portion of the241+Library, but is designed to work with the Library by being compiled or242+linked with it, is called a "work that uses the Library". Such a243+work, in isolation, is not a derivative work of the Library, and244+therefore falls outside the scope of this License.245+246+ However, linking a "work that uses the Library" with the Library247+creates an executable that is a derivative of the Library (because it248+contains portions of the Library), rather than a "work that uses the249+library". The executable is therefore covered by this License.250+Section 6 states terms for distribution of such executables.251+252+ When a "work that uses the Library" uses material from a header file253+that is part of the Library, the object code for the work may be a254+derivative work of the Library even though the source code is not.255+Whether this is true is especially significant if the work can be256+linked without the Library, or if the work is itself a library. The257+threshold for this to be true is not precisely defined by law.258+259+ If such an object file uses only numerical parameters, data260+structure layouts and accessors, and small macros and small inline261+functions (ten lines or less in length), then the use of the object262+file is unrestricted, regardless of whether it is legally a derivative263+work. (Executables containing this object code plus portions of the264+Library will still fall under Section 6.)265+266+ Otherwise, if the work is a derivative of the Library, you may267+distribute the object code for the work under the terms of Section 6.268+Any executables containing that work also fall under Section 6,269+whether or not they are linked directly with the Library itself.270+ 271+ 6. As an exception to the Sections above, you may also combine or272+link a "work that uses the Library" with the Library to produce a273+work containing portions of the Library, and distribute that work274+under terms of your choice, provided that the terms permit275+modification of the work for the customer's own use and reverse276+engineering for debugging such modifications.277+278+ You must give prominent notice with each copy of the work that the279+Library is used in it and that the Library and its use are covered by280+this License. You must supply a copy of this License. If the work281+during execution displays copyright notices, you must include the282+copyright notice for the Library among them, as well as a reference283+directing the user to the copy of this License. Also, you must do one284+of these things:285+286+ a) Accompany the work with the complete corresponding287+ machine-readable source code for the Library including whatever288+ changes were used in the work (which must be distributed under289+ Sections 1 and 2 above); and, if the work is an executable linked290+ with the Library, with the complete machine-readable "work that291+ uses the Library", as object code and/or source code, so that the292+ user can modify the Library and then relink to produce a modified293+ executable containing the modified Library. (It is understood294+ that the user who changes the contents of definitions files in the295+ Library will not necessarily be able to recompile the application296+ to use the modified definitions.)297+298+ b) Use a suitable shared library mechanism for linking with the299+ Library. A suitable mechanism is one that (1) uses at run time a300+ copy of the library already present on the user's computer system,301+ rather than copying library functions into the executable, and (2)302+ will operate properly with a modified version of the library, if303+ the user installs one, as long as the modified version is304+ interface-compatible with the version that the work was made with.305+306+ c) Accompany the work with a written offer, valid for at307+ least three years, to give the same user the materials308+ specified in Subsection 6a, above, for a charge no more309+ than the cost of performing this distribution.310+311+ d) If distribution of the work is made by offering access to copy312+ from a designated place, offer equivalent access to copy the above313+ specified materials from the same place.314+315+ e) Verify that the user has already received a copy of these316+ materials or that you have already sent this user a copy.317+318+ For an executable, the required form of the "work that uses the319+Library" must include any data and utility programs needed for320+reproducing the executable from it. However, as a special exception,321+the materials to be distributed need not include anything that is322+normally distributed (in either source or binary form) with the major323+components (compiler, kernel, and so on) of the operating system on324+which the executable runs, unless that component itself accompanies325+the executable.326+327+ It may happen that this requirement contradicts the license328+restrictions of other proprietary libraries that do not normally329+accompany the operating system. Such a contradiction means you cannot330+use both them and the Library together in an executable that you331+distribute.332+ 333+ 7. You may place library facilities that are a work based on the334+Library side-by-side in a single library together with other library335+facilities not covered by this License, and distribute such a combined336+library, provided that the separate distribution of the work based on337+the Library and of the other library facilities is otherwise338+permitted, and provided that you do these two things:339+340+ a) Accompany the combined library with a copy of the same work341+ based on the Library, uncombined with any other library342+ facilities. This must be distributed under the terms of the343+ Sections above.344+345+ b) Give prominent notice with the combined library of the fact346+ that part of it is a work based on the Library, and explaining347+ where to find the accompanying uncombined form of the same work.348+349+ 8. You may not copy, modify, sublicense, link with, or distribute350+the Library except as expressly provided under this License. Any351+attempt otherwise to copy, modify, sublicense, link with, or352+distribute the Library is void, and will automatically terminate your353+rights under this License. However, parties who have received copies,354+or rights, from you under this License will not have their licenses355+terminated so long as such parties remain in full compliance.356+357+ 9. You are not required to accept this License, since you have not358+signed it. However, nothing else grants you permission to modify or359+distribute the Library or its derivative works. These actions are360+prohibited by law if you do not accept this License. Therefore, by361+modifying or distributing the Library (or any work based on the362+Library), you indicate your acceptance of this License to do so, and363+all its terms and conditions for copying, distributing or modifying364+the Library or works based on it.365+366+ 10. Each time you redistribute the Library (or any work based on the367+Library), the recipient automatically receives a license from the368+original licensor to copy, distribute, link with or modify the Library369+subject to these terms and conditions. You may not impose any further370+restrictions on the recipients' exercise of the rights granted herein.371+You are not responsible for enforcing compliance by third parties with372+this License.373+ 374+ 11. If, as a consequence of a court judgment or allegation of patent375+infringement or for any other reason (not limited to patent issues),376+conditions are imposed on you (whether by court order, agreement or377+otherwise) that contradict the conditions of this License, they do not378+excuse you from the conditions of this License. If you cannot379+distribute so as to satisfy simultaneously your obligations under this380+License and any other pertinent obligations, then as a consequence you381+may not distribute the Library at all. For example, if a patent382+license would not permit royalty-free redistribution of the Library by383+all those who receive copies directly or indirectly through you, then384+the only way you could satisfy both it and this License would be to385+refrain entirely from distribution of the Library.386+387+If any portion of this section is held invalid or unenforceable under any388+particular circumstance, the balance of the section is intended to apply,389+and the section as a whole is intended to apply in other circumstances.390+391+It is not the purpose of this section to induce you to infringe any392+patents or other property right claims or to contest validity of any393+such claims; this section has the sole purpose of protecting the394+integrity of the free software distribution system which is395+implemented by public license practices. Many people have made396+generous contributions to the wide range of software distributed397+through that system in reliance on consistent application of that398+system; it is up to the author/donor to decide if he or she is willing399+to distribute software through any other system and a licensee cannot400+impose that choice.401+402+This section is intended to make thoroughly clear what is believed to403+be a consequence of the rest of this License.404+405+ 12. If the distribution and/or use of the Library is restricted in406+certain countries either by patents or by copyrighted interfaces, the407+original copyright holder who places the Library under this License may add408+an explicit geographical distribution limitation excluding those countries,409+so that distribution is permitted only in or among countries not thus410+excluded. In such case, this License incorporates the limitation as if411+written in the body of this License.412+413+ 13. The Free Software Foundation may publish revised and/or new414+versions of the Lesser General Public License from time to time.415+Such new versions will be similar in spirit to the present version,416+but may differ in detail to address new problems or concerns.417+418+Each version is given a distinguishing version number. If the Library419+specifies a version number of this License which applies to it and420+"any later version", you have the option of following the terms and421+conditions either of that version or of any later version published by422+the Free Software Foundation. If the Library does not specify a423+license version number, you may choose any version ever published by424+the Free Software Foundation.425+ 426+ 14. If you wish to incorporate parts of the Library into other free427+programs whose distribution conditions are incompatible with these,428+write to the author to ask for permission. For software which is429+copyrighted by the Free Software Foundation, write to the Free430+Software Foundation; we sometimes make exceptions for this. Our431+decision will be guided by the two goals of preserving the free status432+of all derivatives of our free software and of promoting the sharing433+and reuse of software generally.434+435+ NO WARRANTY436+437+ 15. BECAUSE THE LIBRARY IS LICENSED FREE OF CHARGE, THERE IS NO438+WARRANTY FOR THE LIBRARY, TO THE EXTENT PERMITTED BY APPLICABLE LAW.439+EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR440+OTHER PARTIES PROVIDE THE LIBRARY "AS IS" WITHOUT WARRANTY OF ANY441+KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE442+IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR443+PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE444+LIBRARY IS WITH YOU. SHOULD THE LIBRARY PROVE DEFECTIVE, YOU ASSUME445+THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION.446+447+ 16. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN448+WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY449+AND/OR REDISTRIBUTE THE LIBRARY AS PERMITTED ABOVE, BE LIABLE TO YOU450+FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR451+CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE452+LIBRARY (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING453+RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A454+FAILURE OF THE LIBRARY TO OPERATE WITH ANY OTHER SOFTWARE), EVEN IF455+SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH456+DAMAGES.457+458+ END OF TERMS AND CONDITIONS
A LICENSES/LIBRAW-COPYRIGHT.txt +27 -0
M NOTICE.md +6 -0
@@ -15,6 +15,12 @@ Repository: https://github.com/darktable-org/darktable
1515 1616 Original notices are retained in the source. Both GPL and AGPL license texts are supplied. GPLv3 §13 and AGPLv3 §13 address their combination. The public repository at https://github.com/FelineStateMachine/gudpics contains the preferred editable forms and build instructions; keep it available when distributing this combined work. No paid or proprietary assets are used.1717 18+## Fuji tool19+20+- LibRaw (https://github.com/LibRaw/LibRaw), revision `116924e5bcb9483ef9c5fbe7f3cfc5a8a4b97064`, unmodified, compiled to WebAssembly with the thin wrapper in `native/raw/wrapper.cpp`. LibRaw is used under the LGPL-2.1; see `LICENSES/LGPL-2.1.txt` and `LICENSES/LIBRAW-COPYRIGHT.txt`. Obtain its source at the URL above at the pinned revision.21+- Film simulation LUTs in `assets/luts/` (provia, velvia, astia, classic_chrome, pro_neg_std, pro_neg_high, eterna, acros, mono and filter variants, sepia): Bastian Bechtold, https://github.com/bastibe/Fujifilm-Auto-Settings-for-Darktable revision `79a458ce9bc28c870091c978b829aaed8dbdb45a`, GPL, unmodified. They were fitted from a Fujifilm camera's own renders against darktable's default render and approximate the camera looks.22+- `assets/luts/neutral-dr*.png`: generated by `scripts/neutral-lut.py` using darktable-cli 5.6.1 (GPL-3.0-or-later). They map LibRaw's render to darktable's default scene-referred render so the film simulation LUTs apply as intended. Film simulation names are Fujifilm trademarks used to identify the looks being approximated; this project is not affiliated with Fujifilm.23+1824 ## Extraction changes (2026-09-21)1925 2026 `scripts/extract.py` reproducibly selects darktable's original constants, line classification, vector helpers, homography, RANSAC, model fitness and Nelder–Mead fit from the pinned source. It uses the original LSD detector and simplex implementation.
M README.md +2 -2
@@ -4,7 +4,7 @@ A mobile-first, installable photo toolkit. Source: https://github.com/FelineStat
44 55 Tools:66 7-- `/fuji`: extracts the in-camera JPEG from a Fujifilm RAF byte for byte, shows the film simulation and camera from its EXIF, and saves or shares it. Pure JavaScript, no decoding of raw sensor data.7+- `/fuji`: develops a Fujifilm RAF in the browser with LibRaw (WebAssembly) and applies any of 16 film simulation looks via 3D LUTs, then saves a JPEG. The in-camera JPEG can be viewed and saved too. See NOTICE.md for where the LUTs come from and how close they are.88 - `/perspective`: straightens converging verticals and horizontals using darktable's C algorithms (LSD line detection, RANSAC, perspective fit, homography) compiled to WebAssembly.99 1010 ## Run
@@ -44,7 +44,7 @@ The config serves only `dist/`; it has no backend, secrets, storage bindings, an
4444 ## Layout4545 4646 - `index.html` + `home.mjs`: landing grid of tools, install and about.47-- `fuji.html` + `fuji.mjs` + `raf.mjs`: the Fuji tool. `raf.mjs` parses the RAF container and the Fuji makernote.47+- `fuji.html` + `fuji.mjs` + `rawworker.mjs` + `raf.mjs`: the Fuji tool. `rawworker.mjs` runs LibRaw (`assets/raw.wasm`, built by `native/raw/build.sh`) and the LUTs off the main thread. `raf.mjs` parses the RAF container and the Fuji makernote. `scripts/neutral-lut.py` regenerates the neutral LUTs with darktable-cli.4848 - `perspective.html` + `app.mjs`: the perspective tool UI. `worker.mjs` runs the WebAssembly core off the main thread.4949 - `style.css`: shared styles. Dark, system font, safe-area aware; the tool page never scrolls.5050
A assets/luts/acros-green.png +0 -0

Binary file.

A assets/luts/acros-red.png +0 -0

Binary file.

A assets/luts/acros-yellow.png +0 -0

Binary file.

A assets/luts/acros.png +0 -0

Binary file.

A assets/luts/astia.png +0 -0

Binary file.

A assets/luts/classic_chrome.png +0 -0

Binary file.

A assets/luts/eterna.png +0 -0

Binary file.

A assets/luts/mono-green.png +0 -0

Binary file.

A assets/luts/mono-red.png +0 -0

Binary file.

A assets/luts/mono-yellow.png +0 -0

Binary file.

A assets/luts/mono.png +0 -0

Binary file.

A assets/luts/neutral-dr100.png +0 -0

Binary file.

A assets/luts/neutral-dr200.png +0 -0

Binary file.

A assets/luts/neutral-dr400.png +0 -0

Binary file.

A assets/luts/pro_neg_high.png +0 -0

Binary file.

A assets/luts/pro_neg_std.png +0 -0

Binary file.

A assets/luts/provia.png +0 -0

Binary file.

A assets/luts/sepia.png +0 -0

Binary file.

A assets/luts/velvia.png +0 -0

Binary file.

A assets/raw.mjs +2 -0
@@ -0,0 +1,2 @@
1+async function Module(moduleArg={}){var Module=moduleArg;var ENVIRONMENT_IS_WEB=!!globalThis.window;var ENVIRONMENT_IS_WORKER=!!globalThis.WorkerGlobalScope;var ENVIRONMENT_IS_NODE=globalThis.process?.versions?.node&&globalThis.process?.type!="renderer";if(ENVIRONMENT_IS_NODE){const{createRequire}=await import("node:module");var require=createRequire(import.meta.url)}var programArgs=[];var thisProgram="./this.program";var quit_=(status,toThrow)=>{throw toThrow};var _scriptName=import.meta.url;var scriptDirectory="";function locateFile(path){if(Module["locateFile"]){return Module["locateFile"](path,scriptDirectory)}return scriptDirectory+path}var readAsync,readBinary;if(ENVIRONMENT_IS_NODE){var fs=require("node:fs");if(_scriptName.startsWith("file:")){scriptDirectory=require("node:path").dirname(require("node:url").fileURLToPath(_scriptName))+"/"}readBinary=filename=>{filename=isFileURI(filename)?new URL(filename):filename;var ret=fs.readFileSync(filename);return ret};readAsync=async(filename,binary=true)=>{filename=isFileURI(filename)?new URL(filename):filename;var ret=fs.readFileSync(filename,binary?undefined:"utf8");return ret};if(process.argv.length>1){thisProgram=process.argv[1].replace(/\\/g,"/")}programArgs=process.argv.slice(2);quit_=(status,toThrow)=>{process.exitCode=status;throw toThrow}}else if(ENVIRONMENT_IS_WEB||ENVIRONMENT_IS_WORKER){try{scriptDirectory=new URL(".",_scriptName).href}catch{}{if(ENVIRONMENT_IS_WORKER){readBinary=url=>{var xhr=new XMLHttpRequest;xhr.open("GET",url,false);xhr.responseType="arraybuffer";xhr.send(null);return new Uint8Array(xhr.response)}}readAsync=async url=>{var response=await fetch(url,{credentials:"same-origin"});if(response.ok){return response.arrayBuffer()}throw new Error(response.status+" : "+response.url)}}}else{}var out=console.log.bind(console);var err=console.error.bind(console);var wasmBinary;var ABORT=false;var isFileURI=filename=>filename.startsWith("file://");var runtimeInitialized=false;function getMemoryBuffer(){return wasmMemory.buffer}function updateMemoryViews(){if(HEAP8?.buffer?.resizable)return;var b=getMemoryBuffer();HEAP8=new Int8Array(b);Module["HEAPU8"]=HEAPU8=new Uint8Array(b);Module["HEAPU16"]=HEAPU16=new Uint16Array(b);HEAP32=new Int32Array(b);HEAPU32=new Uint32Array(b)}function preRun(){var preRun=Module["preRun"];if(preRun){if(typeof preRun=="function")preRun=[preRun];onPreRuns.push(...preRun)}callRuntimeCallbacks(onPreRuns)}function initRuntime(){runtimeInitialized=true;wasmExports["n"]()}function postRun(){var postRun=Module["postRun"];if(postRun){if(typeof postRun=="function")postRun=[postRun];onPostRuns.push(...postRun)}callRuntimeCallbacks(onPostRuns)}function abort(what){Module["onAbort"]?.(what);what=`Aborted(${what})`;err(what);ABORT=true;what+=". Build with -sASSERTIONS for more info.";if(runtimeInitialized){___trap()}var e=new WebAssembly.RuntimeError(what);throw e}var wasmBinaryFile;function findWasmBinary(){if(Module["locateFile"]){return locateFile("raw.wasm")}return new URL("raw.wasm",import.meta.url).href}function getBinarySync(file){if(readBinary){return readBinary(file)}throw"both async and sync fetching of the wasm failed"}async function getWasmBinary(binaryFile){if(!wasmBinary){try{var response=await readAsync(binaryFile);return new Uint8Array(response)}catch{}}return getBinarySync(binaryFile)}async function instantiateArrayBuffer(binaryFile,imports){try{var binary=await getWasmBinary(binaryFile);var instance=await WebAssembly.instantiate(binary,imports);return instance}catch(reason){err(`failed to asynchronously prepare wasm: ${reason}`);abort(reason)}}async function instantiateAsync(binary,binaryFile,imports){if(!binary&&!ENVIRONMENT_IS_NODE){try{var response=fetch(binaryFile,{credentials:"same-origin"});var instantiationResult=await WebAssembly.instantiateStreaming(response,imports);return instantiationResult}catch(reason){err(`wasm streaming compile failed: ${reason}`);err("falling back to ArrayBuffer instantiation")}}return instantiateArrayBuffer(binaryFile,imports)}function getWasmImports(){var imports={a:wasmImports};return imports}async function createWasm(){function receiveInstance(instance){wasmExports=instance.exports;assignWasmExports(wasmExports);updateMemoryViews();return wasmExports}function receiveInstantiationResult(result){return receiveInstance(result["instance"])}var info=getWasmImports();var instantiateWasm=Module["instantiateWasm"];if(instantiateWasm){return new Promise(resolve=>{instantiateWasm(info,inst=>resolve(receiveInstance(inst)))})}wasmBinaryFile??=findWasmBinary();var result=await instantiateAsync(wasmBinary,wasmBinaryFile,info);var exports=receiveInstantiationResult(result);return exports}class ExitStatus{name="ExitStatus";constructor(status){this.message=`Program terminated with exit(${status})`;this.status=status}}var HEAP8;var callRuntimeCallbacks=callbacks=>{while(callbacks.length>0){callbacks.shift()(Module)}};var onPostRuns=[];var onPreRuns=[];var noExitRuntime=true;var UTF8Decoder=globalThis.TextDecoder&&new TextDecoder;var findStringEnd=(heapOrArray,idx,maxBytesToRead,ignoreNul)=>{var maxIdx=idx+maxBytesToRead;if(ignoreNul)return maxIdx;while(heapOrArray[idx]&&!(idx>=maxIdx))++idx;return idx};var UTF8ArrayToString=(heapOrArray,idx=0,maxBytesToRead,ignoreNul)=>{var endPtr=findStringEnd(heapOrArray,idx,maxBytesToRead,ignoreNul);if(endPtr-idx>16&&heapOrArray.buffer&&UTF8Decoder){return UTF8Decoder.decode(heapOrArray.subarray(idx,endPtr))}var str="";while(idx<endPtr){var u0=heapOrArray[idx++];if(!(u0&128)){str+=String.fromCharCode(u0);continue}var u1=heapOrArray[idx++]&63;if((u0&224)==192){str+=String.fromCharCode((u0&31)<<6|u1);continue}var u2=heapOrArray[idx++]&63;if((u0&240)==224){u0=(u0&15)<<12|u1<<6|u2}else{u0=(u0&7)<<18|u1<<12|u2<<6|heapOrArray[idx++]&63}if(u0<65536){str+=String.fromCharCode(u0)}else{var ch=u0-65536;str+=String.fromCharCode(55296|ch>>10,56320|ch&1023)}}return str};var HEAPU8;var UTF8ToString=(ptr,maxBytesToRead,ignoreNul)=>ptr?UTF8ArrayToString(HEAPU8,ptr,maxBytesToRead,ignoreNul):"";var SYSCALLS={varargs:undefined,getStr(ptr){var ret=UTF8ToString(ptr);return ret}};function ___syscall_fcntl64(fd,cmd,varargs){SYSCALLS.varargs=varargs;return 0}function ___syscall_ioctl(fd,op,varargs){SYSCALLS.varargs=varargs;return 0}function ___syscall_openat(dirfd,path,flags,varargs){SYSCALLS.varargs=varargs}var __abort_js=()=>abort("");var isLeapYear=year=>year%4===0&&(year%100!==0||year%400===0);var MONTH_DAYS_LEAP_CUMULATIVE=[0,31,60,91,121,152,182,213,244,274,305,335];var MONTH_DAYS_REGULAR_CUMULATIVE=[0,31,59,90,120,151,181,212,243,273,304,334];var ydayFromDate=date=>{var leap=isLeapYear(date.getFullYear());var monthDaysCumulative=leap?MONTH_DAYS_LEAP_CUMULATIVE:MONTH_DAYS_REGULAR_CUMULATIVE;var yday=monthDaysCumulative[date.getMonth()]+date.getDate()-1;return yday};var INT53_MAX=9007199254740992;var INT53_MIN=-9007199254740992;var bigintToI53Checked=num=>num<INT53_MIN||num>INT53_MAX?NaN:Number(num);var HEAP32;function __localtime_js(time,tmPtr){time=bigintToI53Checked(time);var date=new Date(time*1e3);if(isNaN(date.getTime())){return 1}HEAP32[tmPtr>>2]=date.getSeconds();HEAP32[tmPtr+4>>2]=date.getMinutes();HEAP32[tmPtr+8>>2]=date.getHours();HEAP32[tmPtr+12>>2]=date.getDate();HEAP32[tmPtr+16>>2]=date.getMonth();HEAP32[tmPtr+20>>2]=date.getFullYear()-1900;HEAP32[tmPtr+24>>2]=date.getDay();var yday=ydayFromDate(date)|0;HEAP32[tmPtr+28>>2]=yday;HEAP32[tmPtr+36>>2]=-(date.getTimezoneOffset()*60);var start=new Date(date.getFullYear(),0,1);var summerOffset=new Date(date.getFullYear(),6,1).getTimezoneOffset();var winterOffset=start.getTimezoneOffset();var dst=(summerOffset!=winterOffset&&date.getTimezoneOffset()==Math.min(winterOffset,summerOffset))|0;HEAP32[tmPtr+32>>2]=dst;return 0}var __mktime_js=function(tmPtr){var ret=(()=>{var date=new Date(HEAP32[tmPtr+20>>2]+1900,HEAP32[tmPtr+16>>2],HEAP32[tmPtr+12>>2],HEAP32[tmPtr+8>>2],HEAP32[tmPtr+4>>2],HEAP32[tmPtr>>2],0);if(isNaN(date.getTime())){return-1}var dst=HEAP32[tmPtr+32>>2];var guessedOffset=date.getTimezoneOffset();var start=new Date(date.getFullYear(),0,1);var summerOffset=new Date(date.getFullYear(),6,1).getTimezoneOffset();var winterOffset=start.getTimezoneOffset();var dstOffset=Math.min(winterOffset,summerOffset);if(dst<0){dst=Number(summerOffset!=winterOffset&&dstOffset==guessedOffset)}else if(dst>0!=(dstOffset==guessedOffset)){var nonDstOffset=Math.max(winterOffset,summerOffset);var trueOffset=dst>0?dstOffset:nonDstOffset;date.setTime(date.getTime()+(trueOffset-guessedOffset)*6e4);if(isNaN(date.getTime())){return-1}}HEAP32[tmPtr+32>>2]=dst;HEAP32[tmPtr+24>>2]=date.getDay();var yday=ydayFromDate(date)|0;HEAP32[tmPtr+28>>2]=yday;HEAP32[tmPtr>>2]=date.getSeconds();HEAP32[tmPtr+4>>2]=date.getMinutes();HEAP32[tmPtr+8>>2]=date.getHours();HEAP32[tmPtr+12>>2]=date.getDate();HEAP32[tmPtr+16>>2]=date.getMonth();HEAP32[tmPtr+20>>2]=date.getYear();return date.getTime()/1e3})();return BigInt(ret)};var stringToUTF8Array=(str,heap,outIdx,maxBytesToWrite)=>{if(!(maxBytesToWrite>0))return 0;var startIdx=outIdx;var endIdx=outIdx+maxBytesToWrite-1;for(var i=0;i<str.length;++i){var u=str.codePointAt(i);if(u<=127){if(outIdx>=endIdx)break;heap[outIdx++]=u}else if(u<=2047){if(outIdx+1>=endIdx)break;heap[outIdx++]=192|u>>6;heap[outIdx++]=128|u&63}else if(u<=65535){if(outIdx+2>=endIdx)break;heap[outIdx++]=224|u>>12;heap[outIdx++]=128|u>>6&63;heap[outIdx++]=128|u&63}else{if(outIdx+3>=endIdx)break;heap[outIdx++]=240|u>>18;heap[outIdx++]=128|u>>12&63;heap[outIdx++]=128|u>>6&63;heap[outIdx++]=128|u&63;i++}}heap[outIdx]=0;return outIdx-startIdx};var stringToUTF8=(str,outPtr,maxBytesToWrite)=>stringToUTF8Array(str,HEAPU8,outPtr,maxBytesToWrite);var HEAPU32;var __tzset_js=(timezone,daylight,std_name,dst_name)=>{var currentYear=(new Date).getFullYear();var winter=new Date(currentYear,0,1);var summer=new Date(currentYear,6,1);var winterOffset=winter.getTimezoneOffset();var summerOffset=summer.getTimezoneOffset();var stdTimezoneOffset=Math.max(winterOffset,summerOffset);HEAPU32[timezone>>2]=stdTimezoneOffset*60;HEAP32[daylight>>2]=Number(winterOffset!=summerOffset);var extractZone=timezoneOffset=>{var sign=timezoneOffset>=0?"-":"+";var absOffset=Math.abs(timezoneOffset);var hours=String(Math.floor(absOffset/60)).padStart(2,"0");var minutes=String(absOffset%60).padStart(2,"0");return`UTC${sign}${hours}${minutes}`};var winterName=extractZone(winterOffset);var summerName=extractZone(summerOffset);if(summerOffset<winterOffset){stringToUTF8(winterName,std_name,17);stringToUTF8(summerName,dst_name,17)}else{stringToUTF8(winterName,dst_name,17);stringToUTF8(summerName,std_name,17)}};var getHeapMax=()=>1073741824;var alignMemory=(size,alignment)=>Math.ceil(size/alignment)*alignment;var growMemory=size=>{var oldHeapSize=wasmMemory.buffer.byteLength;var pages=(size-oldHeapSize+65535)/65536|0;try{wasmMemory.grow(pages);updateMemoryViews();return 1}catch(e){}};var _emscripten_resize_heap=requestedSize=>{var oldSize=HEAPU8.length;requestedSize>>>=0;var maxHeapSize=getHeapMax();if(requestedSize>maxHeapSize){return false}for(var cutDown=1;cutDown<=4;cutDown*=2){var overGrownHeapSize=oldSize*(1+.2/cutDown);overGrownHeapSize=Math.min(overGrownHeapSize,requestedSize+100663296);var newSize=Math.min(maxHeapSize,alignMemory(Math.max(requestedSize,overGrownHeapSize),65536));var replacement=growMemory(newSize);if(replacement){return true}}return false};var _fd_close=fd=>52;var _fd_read=(fd,iov,iovcnt,pnum)=>52;function _fd_seek(fd,offset,whence,newOffset){offset=bigintToI53Checked(offset);return 70}var printCharBuffers=[null,[],[]];var printChar=(stream,curr)=>{var buffer=printCharBuffers[stream];if(!curr||curr===10){(stream===1?out:err)(UTF8ArrayToString(buffer));buffer.length=0}else{buffer.push(curr)}};var _fd_write=(fd,iov,iovcnt,pnum)=>{var num=0;for(var i=0;i<iovcnt;i++){var ptr=HEAPU32[iov>>2];var len=HEAPU32[iov+4>>2];iov+=8;for(var j=0;j<len;j++){printChar(fd,HEAPU8[ptr+j])}num+=len}HEAPU32[pnum>>2]=num;return 0};var HEAPU16;{if(Module["noExitRuntime"])noExitRuntime=Module["noExitRuntime"];if(Module["print"])out=Module["print"];if(Module["printErr"])err=Module["printErr"];if(Module["arguments"])programArgs=Module["arguments"];if(Module["thisProgram"])thisProgram=Module["thisProgram"];var preInit=Module["preInit"];if(preInit){if(typeof preInit=="function")Module["preInit"]=preInit=[preInit];while(preInit.length>0){preInit.shift()()}}}Module["UTF8ToString"]=UTF8ToString;var _free,_malloc,_raw_open,_raw_make,_raw_model,_raw_width,_raw_height,_raw_film_mode,_raw_is_xtrans,_raw_dev_dr,_raw_auto_dr,_raw_dr_setting,_raw_develop,_raw_data,_raw_out_width,_raw_out_height,_raw_out_bits,_raw_out_size,_raw_free,___trap,memory,__indirect_function_table,wasmMemory;function assignWasmExports(wasmExports){_free=Module["_free"]=wasmExports["o"];_malloc=Module["_malloc"]=wasmExports["p"];_raw_open=Module["_raw_open"]=wasmExports["q"];_raw_make=Module["_raw_make"]=wasmExports["r"];_raw_model=Module["_raw_model"]=wasmExports["s"];_raw_width=Module["_raw_width"]=wasmExports["t"];_raw_height=Module["_raw_height"]=wasmExports["u"];_raw_film_mode=Module["_raw_film_mode"]=wasmExports["v"];_raw_is_xtrans=Module["_raw_is_xtrans"]=wasmExports["w"];_raw_dev_dr=Module["_raw_dev_dr"]=wasmExports["x"];_raw_auto_dr=Module["_raw_auto_dr"]=wasmExports["y"];_raw_dr_setting=Module["_raw_dr_setting"]=wasmExports["z"];_raw_develop=Module["_raw_develop"]=wasmExports["A"];_raw_data=Module["_raw_data"]=wasmExports["B"];_raw_out_width=Module["_raw_out_width"]=wasmExports["C"];_raw_out_height=Module["_raw_out_height"]=wasmExports["D"];_raw_out_bits=Module["_raw_out_bits"]=wasmExports["E"];_raw_out_size=Module["_raw_out_size"]=wasmExports["F"];_raw_free=Module["_raw_free"]=wasmExports["G"];___trap=wasmExports["H"];memory=wasmMemory=wasmExports["m"];__indirect_function_table=wasmExports["__indirect_function_table"]}var wasmImports={c:___syscall_fcntl64,i:___syscall_ioctl,l:___syscall_openat,k:__abort_js,d:__localtime_js,e:__mktime_js,f:__tzset_js,j:_emscripten_resize_heap,a:_fd_close,h:_fd_read,g:_fd_seek,b:_fd_write};async function run(){preRun();var setStatus=Module["setStatus"];if(setStatus){setStatus("Running...");await new Promise(resolve=>setTimeout(resolve,1));setTimeout(setStatus,1,"")}if(ABORT)return;initRuntime();Module["onRuntimeInitialized"]?.();postRun()}var wasmExports;wasmExports=await createWasm();await run();2+;return Module}export default Module;
A assets/raw.wasm +0 -0

Binary file.

M fuji.html +19 -9
@@ -5,7 +5,7 @@
55 <meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover, interactive-widget=resizes-content">66 <script src="./viewport.js"></script>77 <meta name="theme-color" content="#1a1a1a">8- <meta name="description" content="Pull the in-camera JPEG out of a Fujifilm RAF. Runs on your device.">8+ <meta name="description" content="Develop a Fujifilm RAF with any film simulation. Runs on your device.">99 <meta name="apple-mobile-web-app-capable" content="yes">1010 <meta name="mobile-web-app-capable" content="yes">1111 <meta name="apple-mobile-web-app-status-bar-style" content="black-translucent">
@@ -25,22 +25,21 @@
2525 </header>2626 2727 <section class="stage" id="stage" aria-label="Photo">28- <div class="canvas-wrap"><img id="photo" alt="Camera JPEG"></div>28+ <div class="canvas-wrap"><canvas id="canvas" aria-label="Developed photo"></canvas><img id="photo" alt="Camera JPEG" hidden></div>2929 <button class="empty" id="open-empty" aria-label="Open RAF">3030 <svg width="64" height="64" viewBox="0 0 56 56" aria-hidden="true"><path d="M14 6h20l10 10v34H14z" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linejoin="round"/><path d="M34 6v10h10" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linejoin="round"/><text x="28" y="40" text-anchor="middle" font-size="13" font-weight="700" fill="currentColor" font-family="-apple-system,system-ui,sans-serif">RAF</text></svg>3131 <svg class="plus" width="28" height="28" viewBox="0 0 28 28" aria-hidden="true"><path d="M14 5v18M5 14h18" fill="none" stroke="currentColor" stroke-width="2.6" stroke-linecap="round"/></svg>3232 </button>33+ <div class="hud">34+ <span class="chip" id="info"></span>35+ <button class="compare" id="compare" aria-pressed="false" aria-label="Show camera JPEG" disabled><svg width="20" height="20" viewBox="0 0 20 20" aria-hidden="true"><path d="M10 2v16M7 5H3v10h4M13 5h4v10h-4" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"/></svg></button>36+ </div>3337 <div class="toast" id="status" role="status" aria-live="polite" hidden></div>3438 <div class="busy" id="busy" hidden><span class="spinner"></span></div>3539 </section>3640 37- <section class="panel" aria-label="Details">38- <dl class="facts" id="facts">39- <div><dt>Camera</dt><dd id="camera">-</dd></div>40- <div><dt>Simulation</dt><dd id="simulation">-</dd></div>41- <div><dt>Size</dt><dd id="size">-</dd></div>42- <div><dt>Taken</dt><dd id="date">-</dd></div>43- </dl>41+ <section class="panel" aria-label="Film simulation">42+ <div class="chips" id="sims" role="radiogroup" aria-label="Film simulation"></div>4443 <div class="toolbar">4544 <button class="tb" id="open" aria-label="Open"><svg viewBox="0 0 24 24" aria-hidden="true"><rect x="3" y="5" width="18" height="14" rx="2.5" fill="none" stroke="currentColor" stroke-width="1.8"/><circle cx="8.5" cy="10" r="1.8" fill="currentColor"/><path d="M4 17l5-5 4 4 2.5-2.5L20 18" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linejoin="round"/></svg></button>4645 <button class="tb" id="share" aria-label="Share" disabled hidden><svg viewBox="0 0 24 24" aria-hidden="true"><path d="M12 3v13M8 7l4-4 4 4M5 12v7a2 2 0 0 0 2 2h10a2 2 0 0 0 2-2v-7" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"/></svg></button>
@@ -48,6 +47,17 @@
4847 </section>4948 <input id="file" type="file" hidden>5049 </div>50+51+ <dialog id="save-sheet" class="sheet" aria-label="Save">52+ <div class="sheet-body actions" tabindex="-1" autofocus>53+ <div class="grabber"></div>54+ <div class="rows">55+ <button class="row" data-save="developed">Developed JPG</button>56+ <button class="row" data-save="camera">Camera JPG</button>57+ </div>58+ <button class="cancel" data-close>Cancel</button>59+ </div>60+ </dialog>5161 <script type="module" src="./fuji.mjs"></script>5262 </body>5363 </html>
M fuji.mjs +88 -29
@@ -1,34 +1,80 @@
11 // SPDX-License-Identifier: AGPL-3.0-or-later22 import {parseRaf} from './raf.mjs';33 const $=id=>document.getElementById(id);4-const tool=$('tool'),photo=$('photo');5-let current=null,filename='photo',busy=false,toastTimer,previewUrl;4+const worker=new Worker(new URL('./rawworker.mjs',import.meta.url),{type:'module'});5+let nextId=0;const pending=new Map();6+worker.onmessage=({data})=>{const p=pending.get(data.id);if(!p)return;pending.delete(data.id);data.error?p.reject(Error(data.error)):p.resolve(data.result);};7+worker.onerror=()=>{for(const p of pending.values())p.reject(Error('The raw engine stopped. Reload to try again.'));pending.clear();};8+const rpc=(type,args={},transfer=[])=>new Promise((resolve,reject)=>{const id=++nextId;pending.set(id,{resolve,reject});worker.postMessage({type,id,...args},transfer);});9+10+// Film simulation LUTs (GPL, see NOTICE.md). Names are the looks being approximated.11+const SIMS=[12+ ['provia','Provia'],['velvia','Velvia'],['astia','Astia'],['classic_chrome','Classic Chrome'],['pro_neg_high','Pro Neg. Hi'],['pro_neg_std','Pro Neg. Std'],['eterna','Eterna'],13+ ['acros','Acros'],['acros-red','Acros+R'],['acros-yellow','Acros+Ye'],['acros-green','Acros+G'],['mono','Mono'],['mono-red','Mono+R'],['mono-yellow','Mono+Ye'],['mono-green','Mono+G'],['sepia','Sepia']];14+const LUT_URLS={15+ provia:new URL('./assets/luts/provia.png',import.meta.url),velvia:new URL('./assets/luts/velvia.png',import.meta.url),astia:new URL('./assets/luts/astia.png',import.meta.url),16+ classic_chrome:new URL('./assets/luts/classic_chrome.png',import.meta.url),pro_neg_high:new URL('./assets/luts/pro_neg_high.png',import.meta.url),pro_neg_std:new URL('./assets/luts/pro_neg_std.png',import.meta.url),17+ eterna:new URL('./assets/luts/eterna.png',import.meta.url),acros:new URL('./assets/luts/acros.png',import.meta.url),'acros-red':new URL('./assets/luts/acros-red.png',import.meta.url),18+ 'acros-yellow':new URL('./assets/luts/acros-yellow.png',import.meta.url),'acros-green':new URL('./assets/luts/acros-green.png',import.meta.url),mono:new URL('./assets/luts/mono.png',import.meta.url),19+ 'mono-red':new URL('./assets/luts/mono-red.png',import.meta.url),'mono-yellow':new URL('./assets/luts/mono-yellow.png',import.meta.url),'mono-green':new URL('./assets/luts/mono-green.png',import.meta.url),20+ sepia:new URL('./assets/luts/sepia.png',import.meta.url)};21+const NEUTRAL_URLS={100:new URL('./assets/luts/neutral-dr100.png',import.meta.url),200:new URL('./assets/luts/neutral-dr200.png',import.meta.url),400:new URL('./assets/luts/neutral-dr400.png',import.meta.url)};22+// Camera film mode and monochrome names (from the RAF makernote) to our LUT names.23+const FROM_CAMERA={'Provia':'provia','Velvia':'velvia','Astia':'astia','Classic Chrome':'classic_chrome','Pro Neg. Hi':'pro_neg_high','Pro Neg. Std':'pro_neg_std','Eterna':'eterna',24+ 'Acros':'acros','Acros+R':'acros-red','Acros+Ye':'acros-yellow','Acros+G':'acros-green','Monochrome':'mono','Monochrome+R':'mono-red','Monochrome+Ye':'mono-yellow','Monochrome+G':'mono-green','Sepia':'sepia'};25+26+const tool=$('tool'),canvas=$('canvas'),ctx=canvas.getContext('2d'),photo=$('photo');27+let current=null,busy=false,toastTimer,sim='provia',dr=100,showCamera=false,holdCamera=false,renderId=0,cameraUrl=null;28+const loaded=new Set(),loading=new Map();29+630 function status(message,error=false){clearTimeout(toastTimer);const el=$('status');el.textContent=message;el.classList.toggle('error',error);el.hidden=!message;if(message)toastTimer=setTimeout(()=>{el.hidden=true;},error?5000:2500);}731 function sync(){8- tool.dataset.state=current?'loaded':'empty';9- $('save').disabled=!current||busy;$('share').disabled=!current||busy;$('open').disabled=busy;$('open-empty').disabled=busy;32+ tool.dataset.state=current?'detected':'empty';33+ $('save').disabled=!current||busy;$('share').disabled=!current||busy;$('open').disabled=busy;$('open-empty').disabled=busy;$('compare').disabled=!current||!current.camera;1034 $('busy').hidden=!busy;35+ for(const b of $('sims').children){b.setAttribute('aria-checked',String(b.dataset.sim===sim));b.disabled=!current||busy;}36+ const cam=(showCamera||holdCamera)&&current?.camera;37+ canvas.hidden=Boolean(cam);photo.hidden=!cam;$('compare').setAttribute('aria-pressed',String(Boolean(cam)));38+}39+async function decodeLut(url){40+ const bitmap=await createImageBitmap(await (await fetch(url)).blob());41+ const c=document.createElement('canvas');c.width=bitmap.width;c.height=bitmap.height;const x=c.getContext('2d');x.drawImage(bitmap,0,0);42+ const d=x.getImageData(0,0,c.width,c.height).data,out=new Uint8Array(c.width*c.height*3);43+ for(let i=0,o=0;i<d.length;i+=4,o+=3){out[o]=d[i];out[o+1]=d[i+1];out[o+2]=d[i+2];}44+ return out;45+}46+function ensureLut(kind,name,url){47+ const key=kind+':'+name;if(loaded.has(key))return Promise.resolve();48+ if(!loading.has(key))loading.set(key,decodeLut(url).then(table=>rpc('lut',{kind,name,table},[table.buffer])).then(()=>{loaded.add(key);}).catch(e=>{loading.delete(key);throw e;}));49+ return loading.get(key);50+}51+async function render(){52+ if(!current)return;const id=++renderId;53+ try{54+ await Promise.all([ensureLut('neutral',dr,NEUTRAL_URLS[dr]||NEUTRAL_URLS[100]),ensureLut('sim',sim,LUT_URLS[sim])]);55+ const r=await rpc('render',{dr:NEUTRAL_URLS[dr]?dr:100,sim});56+ if(id!==renderId)return;57+ canvas.width=r.w;canvas.height=r.h;ctx.putImageData(new ImageData(r.data,r.w,r.h),0,0);58+ }catch(e){status(e.message,true);}1159 }12-function fmtDate(s){const m=/^(\d{4}):(\d{2}):(\d{2}) (\d{2}):(\d{2})/.exec(s||'');return m?`${m[1]}-${m[2]}-${m[3]} ${m[4]}:${m[5]}`:'-';}1360 async function openFile(file){1461 if(busy)return;busy=true;sync();1562 try{1663 if(file.size>200*1024*1024)throw Error('Choose a RAF smaller than 200 MB.');17- const r=parseRaf(await file.arrayBuffer());18- if(previewUrl)URL.revokeObjectURL(previewUrl);19- const blob=new Blob([r.jpeg],{type:'image/jpeg'});20- previewUrl=URL.createObjectURL(blob);21- await new Promise((res,rej)=>{photo.onload=res;photo.onerror=()=>rej(Error('The embedded JPEG could not be decoded.'));photo.src=previewUrl;});22- current={blob,name:file.name.replace(/\.[^.]+$/,'')+'.jpg'};23- filename=current.name;24- $('camera').textContent=r.camera||r.model||'-';25- $('simulation').textContent=r.simulation||'-';26- $('size').textContent=r.width?`${r.width} x ${r.height}, ${(r.jpeg.length/1e6).toFixed(1)} MB`:`${(r.jpeg.length/1e6).toFixed(1)} MB`;27- $('date').textContent=fmtDate(r.date);28- status('');64+ const buffer=await file.arrayBuffer();65+ let camera=null,shot='';66+ try{const r=parseRaf(buffer.slice(0));camera=new Blob([r.jpeg],{type:'image/jpeg'});shot=r.simulation;}catch{}67+ const info=await rpc('open',{buffer},[buffer]);68+ if(cameraUrl)URL.revokeObjectURL(cameraUrl);cameraUrl=camera?URL.createObjectURL(camera):null;photo.src=cameraUrl||'';69+ current={name:file.name.replace(/\.[^.]+$/,''),camera,info};70+ dr=[100,200,400].includes(info.dr)?info.dr:100;sim=FROM_CAMERA[shot]||'provia';showCamera=false;71+ $('info').textContent=[info.model,shot,dr!==100?'DR'+dr:''].filter(Boolean).join(' ');$('info').hidden=false;72+ sync();await render();2973 }catch(e){status(e.message||'Could not read this file.',true);}3074 finally{busy=false;sync();}3175 }76+// Chips77+for(const [name,label] of SIMS){const b=document.createElement('button');b.className='chip-btn';b.role='radio';b.dataset.sim=name;b.textContent=label;b.disabled=true;b.onclick=()=>{sim=name;showCamera=false;sync();render();b.scrollIntoView({inline:'center',block:'nearest',behavior:'smooth'});};$('sims').append(b);}3278 $('file').onchange=()=>{const f=$('file').files[0];if(f)openFile(f);$('file').value='';};3379 $('open').onclick=()=>$('file').click();$('open-empty').onclick=()=>$('file').click();3480 const stage=$('stage'),zone=$('open-empty');
@@ -36,19 +82,32 @@ stage.addEventListener('dragover',e=>{e.preventDefault();e.dataTransfer.dropEffe
3682 stage.addEventListener('dragleave',()=>zone.classList.remove('over'));3783 stage.addEventListener('drop',e=>{e.preventDefault();zone.classList.remove('over');const f=e.dataTransfer.files[0];if(f)openFile(f);});3884 stage.addEventListener('contextmenu',e=>e.preventDefault());39-$('save').onclick=()=>{40- if(!current)return;41- const url=URL.createObjectURL(current.blob),a=document.createElement('a');a.href=url;a.download=current.name;document.body.append(a);a.click();a.remove();setTimeout(()=>URL.revokeObjectURL(url),60000);42- status('Saved');43-};44-// Share sheet is the native way to get a file into the photo library on phones.45-const canShare=Boolean(navigator.canShare&&navigator.share);46-if(canShare)$('share').hidden=false;85+stage.addEventListener('pointerdown',e=>{if(!current?.camera||busy||e.target.closest('button'))return;holdCamera=true;sync();});86+for(const t of ['pointerup','pointercancel','pointerleave'])stage.addEventListener(t,()=>{if(!holdCamera)return;holdCamera=false;sync();});87+$('compare').onclick=()=>{showCamera=!showCamera;sync();};88+// Save89+const saveSheet=$('save-sheet');for(const b of saveSheet.querySelectorAll('[data-close]'))b.onclick=()=>saveSheet.close();saveSheet.addEventListener('click',e=>{if(e.target===saveSheet)saveSheet.close();});90+$('save').onclick=()=>saveSheet.showModal();91+for(const b of saveSheet.querySelectorAll('[data-save]'))b.onclick=()=>{saveSheet.close();save(b.dataset.save);};92+function download(blob,name){const url=URL.createObjectURL(blob),a=document.createElement('a');a.href=url;a.download=name;document.body.append(a);a.click();a.remove();setTimeout(()=>URL.revokeObjectURL(url),60000);}93+async function developedBlob(){94+ const r=await rpc('render',{dr:NEUTRAL_URLS[dr]?dr:100,sim,full:true});95+ const c=document.createElement('canvas');c.width=r.w;c.height=r.h;c.getContext('2d').putImageData(new ImageData(r.data,r.w,r.h),0,0);96+ const blob=await new Promise(res=>c.toBlob(res,'image/jpeg',0.94));if(!blob)throw Error('Could not encode the JPEG.');return {blob,w:r.w,h:r.h};97+}98+async function save(what){99+ if(!current)return;busy=true;sync();100+ try{101+ if(what==='camera'){if(!current.camera)throw Error('No camera JPEG in this file.');download(current.camera,current.name+'.jpg');status('Saved camera JPG');}102+ else{const {blob,w,h}=await developedBlob();download(blob,`${current.name}-${sim}.jpg`);status(`Saved ${w} x ${h}`);}103+ }catch(e){status(e.message,true);}finally{busy=false;sync();}104+}105+if(navigator.canShare&&navigator.share)$('share').hidden=false;47106 $('share').onclick=async()=>{48- if(!current)return;49- const file=new File([current.blob],current.name,{type:'image/jpeg'});50- if(!navigator.canShare({files:[file]}))return status('Sharing files is not supported here.',true);51- try{await navigator.share({files:[file]});}catch(e){if(e.name!=='AbortError')status(e.message,true);}107+ if(!current)return;busy=true;sync();108+ try{const {blob}=await developedBlob();const file=new File([blob],`${current.name}-${sim}.jpg`,{type:'image/jpeg'});109+ if(!navigator.canShare({files:[file]}))throw Error('Sharing files is not supported here.');await navigator.share({files:[file]});}110+ catch(e){if(e.name!=='AbortError')status(e.message,true);}finally{busy=false;sync();}52111 };53112 if('serviceWorker'in navigator){navigator.serviceWorker.register('./sw.js').catch(()=>{});let hadController=Boolean(navigator.serviceWorker.controller);navigator.serviceWorker.addEventListener('controllerchange',()=>{if(hadController&&!current)location.reload();hadController=true;});}54113 sync();
M index.html +3 -2
A native/raw/build.sh +14 -0
A native/raw/wrapper.cpp +39 -0
@@ -0,0 +1,39 @@
1+// SPDX-License-Identifier: AGPL-3.0-or-later2+// Thin C ABI over LibRaw for the browser: open a RAF from memory, develop to 8-bit RGB.3+#include "libraw/libraw.h"4+#include <cstring>5+extern "C" {6+static LibRaw* lr = nullptr;7+static libraw_processed_image_t* img = nullptr;8+static void reset_img(){ if(img){ LibRaw::dcraw_clear_mem(img); img=nullptr; } }9+int raw_open(const void* buf, size_t len){10+ reset_img(); if(lr){ lr->recycle(); } else lr = new LibRaw();11+ return lr->open_buffer(const_cast<void*>(buf), len);12+}13+const char* raw_make(){ return lr ? lr->imgdata.idata.make : ""; }14+const char* raw_model(){ return lr ? lr->imgdata.idata.model : ""; }15+int raw_width(){ return lr ? lr->imgdata.sizes.width : 0; }16+int raw_height(){ return lr ? lr->imgdata.sizes.height : 0; }17+int raw_film_mode(){ return lr ? (int)lr->imgdata.makernotes.fuji.FilmMode : 0; }18+int raw_is_xtrans(){ return lr ? (lr->imgdata.idata.filters == 9) : 0; }19+int raw_dev_dr(){ return lr ? (int)lr->imgdata.makernotes.fuji.DevelopmentDynamicRange : 0; }20+int raw_auto_dr(){ return lr ? (int)lr->imgdata.makernotes.fuji.AutoDynamicRange : 0; }21+int raw_dr_setting(){ return lr ? (int)lr->imgdata.makernotes.fuji.DynamicRangeSetting : 0; }22+// gamma: 0 = linear, 1 = sRGB curve. color: 1 sRGB, 8 Rec2020. half: 1 = half size.23+int raw_develop(int half, int gamma, int color, int bps){24+ if(!lr) return -1; reset_img();25+ libraw_output_params_t& p = lr->imgdata.params;26+ p.half_size = half; p.use_camera_wb = 1; p.use_auto_wb = 0; p.output_color = color; p.output_bps = bps;27+ p.no_auto_bright = 1; p.highlight = 0; p.user_qual = half ? 0 : 1; p.fbdd_noiserd = 0; p.med_passes = 0;28+ if(gamma){ p.gamm[0] = 1.0/2.4; p.gamm[1] = 12.92; } else { p.gamm[0] = 1; p.gamm[1] = 1; }29+ int r = lr->unpack(); if(r) return r;30+ r = lr->dcraw_process(); if(r) return r;31+ int err = 0; img = lr->dcraw_make_mem_image(&err); return err;32+}33+unsigned char* raw_data(){ return img ? img->data : nullptr; }34+int raw_out_width(){ return img ? img->width : 0; }35+int raw_out_height(){ return img ? img->height : 0; }36+int raw_out_bits(){ return img ? img->bits : 0; }37+unsigned raw_out_size(){ return img ? img->data_size : 0; }38+void raw_free(){ reset_img(); if(lr){ delete lr; lr=nullptr; } }39+}
A rawworker.mjs +77 -0
@@ -0,0 +1,77 @@
1+// SPDX-License-Identifier: AGPL-3.0-or-later2+// Develops Fujifilm RAF files with LibRaw and applies film simulation LUTs off the main thread.3+import createRaw from './assets/raw.mjs';4+const corePromise=createRaw();5+const N=33; // neutral LUT grid6+let full=null,preview=null; // {w,h,rgb:Uint8Array}7+let luts={neutral:{},sim:{}}; // decoded LUT tables: neutral[dr] = Uint8Array(N^3*3), sim[name] = Uint8Array(16^3*3)8+let composed=new Map(); // key dr:sim -> Uint8Array(N^3*3)9+10+function downscale(src,w,h,factor){11+ const ow=Math.floor(w/factor),oh=Math.floor(h/factor),out=new Uint8Array(ow*oh*3),n=factor*factor;12+ for(let y=0;y<oh;y++)for(let x=0;x<ow;x++){13+ let r=0,g=0,b=0;14+ for(let dy=0;dy<factor;dy++){let i=((y*factor+dy)*w+x*factor)*3;for(let dx=0;dx<factor;dx++){r+=src[i];g+=src[i+1];b+=src[i+2];i+=3;}}15+ const o=(y*ow+x)*3;out[o]=r/n;out[o+1]=g/n;out[o+2]=b/n;16+ }17+ return {w:ow,h:oh,rgb:out};18+}19+// Trilinear sample of a cubic LUT laid out [b][g][r] with `size` points per axis; input coords in [0,size-1].20+function sample(lut,size,r,g,b,out,o){21+ let ri=Math.floor(r),gi=Math.floor(g),bi=Math.floor(b);22+ if(ri>=size-1)ri=size-2;if(gi>=size-1)gi=size-2;if(bi>=size-1)bi=size-2;23+ const fr=r-ri,fg=g-gi,fb=b-bi,s2=size*size;24+ const i000=((bi*size+gi)*size+ri)*3,i100=i000+3,i010=i000+size*3,i110=i010+3,i001=i000+s2*3,i101=i001+3,i011=i001+size*3,i111=i011+3;25+ for(let c=0;c<3;c++){26+ const c00=lut[i000+c]*(1-fr)+lut[i100+c]*fr,c10=lut[i010+c]*(1-fr)+lut[i110+c]*fr;27+ const c01=lut[i001+c]*(1-fr)+lut[i101+c]*fr,c11=lut[i011+c]*(1-fr)+lut[i111+c]*fr;28+ out[o+c]=(c00*(1-fg)+c10*fg)*(1-fb)+(c01*(1-fg)+c11*fg)*fb;29+ }30+}31+function compose(dr,sim){32+ const key=dr+':'+sim;if(composed.has(key))return composed.get(key);33+ const neutral=luts.neutral[dr],simLut=sim==='neutral'?null:luts.sim[sim];34+ if(!neutral||(sim!=='neutral'&&!simLut))throw Error('Look not loaded.');35+ const table=new Uint8Array(N*N*N*3),tmp=new Float32Array(3);36+ for(let i=0;i<N*N*N;i++){37+ const r=neutral[i*3],g=neutral[i*3+1],b=neutral[i*3+2];38+ if(simLut){sample(simLut,16,r/17,g/17,b/17,tmp,0);table[i*3]=tmp[0]+.5;table[i*3+1]=tmp[1]+.5;table[i*3+2]=tmp[2]+.5;}39+ else{table[i*3]=r;table[i*3+1]=g;table[i*3+2]=b;}40+ }41+ composed.set(key,table);return table;42+}43+function render(img,table){44+ const {w,h,rgb}=img,out=new Uint8ClampedArray(w*h*4),tmp=new Float32Array(3),k=(N-1)/255;45+ for(let i=0,o=0;i<rgb.length;i+=3,o+=4){46+ sample(table,N,rgb[i]*k,rgb[i+1]*k,rgb[i+2]*k,tmp,0);47+ out[o]=tmp[0]+.5;out[o+1]=tmp[1]+.5;out[o+2]=tmp[2]+.5;out[o+3]=255;48+ }49+ return {w,h,data:out};50+}51+async function open(buffer){52+ const core=await corePromise;53+ const p=core._malloc(buffer.byteLength);if(!p)throw Error('Not enough memory for this file.');54+ core.HEAPU8.set(new Uint8Array(buffer),p);55+ try{56+ let r=core._raw_open(p,buffer.byteLength);if(r)throw Error('LibRaw could not read this file.');57+ const info={make:core.UTF8ToString(core._raw_make()),model:core.UTF8ToString(core._raw_model()),width:core._raw_width(),height:core._raw_height(),film:core._raw_film_mode(),dr:[core._raw_dev_dr(),core._raw_auto_dr()].find(v=>v===100||v===200||v===400)||100,xtrans:Boolean(core._raw_is_xtrans())};58+ if(!/fuji/i.test(info.make))throw Error('Only Fujifilm RAF files are supported.');59+ r=core._raw_develop(1,1,1,8);if(r)throw Error('Developing failed.');60+ const w=core._raw_out_width(),h=core._raw_out_height(),d=core._raw_data();61+ full={w,h,rgb:core.HEAPU8.slice(d,d+w*h*3)};62+ const factor=Math.max(1,Math.ceil(Math.max(w,h)/1600));63+ preview=factor>1?downscale(full.rgb,w,h,factor):full;64+ return info;65+ }finally{core._raw_free();core._free(p);}66+}67+self.onmessage=async({data})=>{68+ try{69+ let result,transfer=[];70+ if(data.type==='open'){result=await open(data.buffer);}71+ else if(data.type==='lut'){luts[data.kind][data.name]=data.table;composed.clear();result={ok:true};}72+ else if(data.type==='render'){const img=data.full?full:preview;if(!img)throw Error('Open a RAF first.');result=render(img,compose(data.dr,data.sim));transfer=[result.data.buffer];}73+ else if(data.type==='close'){full=preview=null;result={ok:true};}74+ else throw Error('Unknown operation.');75+ self.postMessage({id:data.id,result},transfer);76+ }catch(e){self.postMessage({id:data.id,error:e.message||'Could not process this file.'});}77+};
A scripts/neutral-lut.py +70 -0
@@ -0,0 +1,70 @@
1+"""Generate the neutral LUTs in assets/luts/neutral-dr*.png.2+SPDX-License-Identifier: AGPL-3.0-or-later3+4+A Hald image in the LibRaw output domain (sRGB-encoded, camera white balance, sRGB primaries)5+is wrapped as a LinearRaw DNG carrying an X-Pro3 color matrix and as-shot neutral, then rendered6+through darktable-cli with the default scene-referred (filmic) workflow. The result is the mapping7+from LibRaw's render to darktable's default render, which is the input the film simulation LUTs8+in assets/luts were fitted against. One LUT per Fujifilm DR mode, since darktable compensates9+DR200 and DR400 by one and two stops. Requires darktable-cli, numpy and Pillow.10+"""11+import numpy as np, struct, subprocess, sys12+N=33; BLOCK=413+cam_xyz=[1.3426,-0.6334,-0.1177,-0.4244,1.2136,0.2371,-0.058,0.1303,0.598]14+rgb_cam=np.array([[1.23842,0.0314905,-0.269914],[-0.146979,1.55701,-0.410031],[0.00372626,-0.395365,1.39164]])15+cam_mul=np.array([567.,302.,545.]); wb=cam_mul/cam_mul.min()16+neutral=[302/567,1.0,302/545]17+def srgb_decode(u): return np.where(u<=0.04045,u/12.92,((u+0.055)/1.055)**2.4)18+def write_cam_dng(path,rgb16,white=65535):19+ h,w,_=rgb16.shape; data=rgb16.astype('<u2').tobytes(); entries=[]20+ def add(tag,typ,vals):21+ if typ==2: b=vals.encode()+b'\0'; cnt=len(b)22+ elif typ==3: b=struct.pack('<%dH'%len(vals),*vals); cnt=len(vals)23+ elif typ==4: b=struct.pack('<%dI'%len(vals),*vals); cnt=len(vals)24+ elif typ==1: b=bytes(vals); cnt=len(vals)25+ elif typ==5: b=b''.join(struct.pack('<II',n,d) for n,d in vals); cnt=len(vals)26+ elif typ==10: b=b''.join(struct.pack('<ii',n,d) for n,d in vals); cnt=len(vals)27+ entries.append((tag,typ,cnt,b))28+ add(254,4,[0]); add(256,4,[w]); add(257,4,[h]); add(258,3,[16,16,16]); add(259,3,[1]); add(262,3,[34892])29+ add(271,2,'FUJIFILM'); add(272,2,'X-Pro3'); add(273,4,[0]); add(274,3,[1]); add(277,3,[3]); add(278,4,[h]); add(279,4,[len(data)]); add(284,3,[1])30+ add(50706,1,[1,4,0,0]); add(50707,1,[1,1,0,0]); add(50708,2,'FUJIFILM X-Pro3'); add(50714,4,[0,0,0]); add(50717,4,[white,white,white])31+ add(50721,10,[(int(round(v*1e6)),1000000) for v in cam_xyz]); add(50728,5,[(int(round(n*1e6)),1000000) for n in neutral]); add(50778,3,[21])32+ entries.sort(key=lambda e:e[0]); n=len(entries); ifd_off=8; extra_off=ifd_off+2+n*12+4; extra=b''; ifd=struct.pack('<H',n)33+ for tag,typ,cnt,b in entries:34+ if tag==273: ifd+=struct.pack('<HHII',tag,typ,cnt,0); continue35+ if len(b)<=4: ifd+=struct.pack('<HHI',tag,typ,cnt)+b.ljust(4,b'\0')36+ else: ifd+=struct.pack('<HHII',tag,typ,cnt,extra_off+len(extra)); extra+=b+(b'\0' if len(b)%2 else b'')37+ ifd+=struct.pack('<I',0); data_off=extra_off+len(extra)38+ idx=[e[0] for e in entries].index(273); pos=2+idx*12+8; ifd=ifd[:pos]+struct.pack('<I',data_off)+ifd[pos+4:]39+ open(path,'wb').write(b'II*\0'+struct.pack('<I',ifd_off)+ifd+extra+data)40+def hald_linear():41+ g=np.arange(N)/(N-1)42+ b,gg,r=np.meshgrid(g,g,g,indexing='ij') # index order [b][g][r], r fastest43+ u=np.stack([r,gg,b],axis=-1).reshape(-1,3) # N^3 x 3 encoded sRGB44+ lin=srgb_decode(u)45+ cam=lin@np.linalg.inv(rgb_cam).T # linear sRGB -> camera space (post WB)46+ raw=cam/wb # undo white balance47+ return raw48+def make_dng(gain,path):49+ raw=hald_linear()*gain50+ raw=np.clip(raw,0,0.995) # keep below clipping so highlight reconstruction stays out51+ cells=raw.reshape(N*N,N,3) # rows = b*N+g, cols = r52+ img=np.repeat(np.repeat(cells,BLOCK,axis=0),BLOCK,axis=1)53+ write_cam_dng(path,(img*65535+0.5).astype(np.uint16))54+ return img.shape55+import os56+DT=os.environ.get('DARKTABLE_CLI','/Applications/darktable.app/Contents/MacOS/darktable-cli')57+OUT=os.path.join(os.path.dirname(os.path.abspath(__file__)),'..','assets','luts')58+TMP='/tmp/gudpics-neutral-lut'; os.makedirs(TMP,exist_ok=True)59+for gain,tag in [(1,'dr100'),(2,'dr200'),(4,'dr400')]:60+ shape=make_dng(gain,f'{TMP}/hald_{tag}.dng')61+ subprocess.run(['rm','-f',f'{TMP}/hald_{tag}.png'])62+ r=subprocess.run([DT,f'{TMP}/hald_{tag}.dng',f'{TMP}/hald_{tag}.png','--core','--configdir',TMP+'/dtconf','--conf','plugins/darkroom/workflow=scene-referred (filmic)','--conf','plugins/imageio/format/png/bpp=8'],capture_output=True,text=True)63+ print(tag,'dng',shape,'darktable:',[l for l in (r.stdout+r.stderr).splitlines() if 'export' in l or 'rror' in l][-1:])64+65+from PIL import Image66+for tag in ['dr100','dr200','dr400']:67+ im=np.asarray(Image.open(f'{TMP}/hald_{tag}.png').convert('RGB'),dtype=np.float32)68+ cells=im.reshape(N*N,BLOCK,N,BLOCK,3)[:,1:3,:,1:3,:].mean(axis=(1,3)) # inner pixels of each block69+ Image.fromarray(np.clip(cells+0.5,0,255).astype(np.uint8)).save(os.path.join(OUT,f'neutral-{tag}.png'))70+ print('wrote',f'neutral-{tag}.png',cells.shape)
M scripts/package.py +5 -4
@@ -13,7 +13,8 @@ if dist.exists(): shutil.rmtree(dist)
1313 (dist / 'static').mkdir(parents=True)1414 1515 # Hashed files in dependency order: leaves first, so a file's hash covers its rewritten references.16-HASHED = ['assets/core.wasm', 'assets/core.mjs', 'geometry.mjs', 'worker.mjs', 'app.mjs', 'raf.mjs', 'fuji.mjs', 'home.mjs', 'viewport.js', 'style.css']16+LUTS = sorted(p.relative_to(root).as_posix() for p in (root / 'assets' / 'luts').glob('*.png'))17+HASHED = ['assets/core.wasm', 'assets/core.mjs', 'assets/raw.wasm', 'assets/raw.mjs'] + LUTS + ['geometry.mjs', 'worker.mjs', 'app.mjs', 'raf.mjs', 'rawworker.mjs', 'fuji.mjs', 'home.mjs', 'viewport.js', 'style.css']1718 # Fixed-name text files that reference hashed ones.1819 PAGES = ['index.html', 'perspective.html', 'fuji.html']1920 FIXED = ['manifest.webmanifest', 'NOTICE.md', 'README.md', '_headers']
@@ -33,10 +34,10 @@ def rewrite(text, at_dir):
3334 for src in HASHED:3435 p = root / src3536 data = p.read_bytes()36- if p.suffix != '.wasm':37+ if p.suffix not in ('.wasm', '.png'):3738 text = rewrite(data.decode(), 'static')38- if src == 'assets/core.mjs': # emscripten loader refers to its binary by bare name39- text = text.replace('"core.wasm"', '"' + Path(mapping['assets/core.wasm']).name + '"')39+ if src in ('assets/core.mjs', 'assets/raw.mjs'): # emscripten loader refers to its binary by bare name40+ wasm = src.replace('.mjs', '.wasm'); text = text.replace('"' + Path(wasm).name + '"', '"' + Path(mapping[wasm]).name + '"')4041 data = text.encode()4142 name = f'{p.stem}.{digest(data)}{p.suffix}'4243 mapping[src] = f'static/{name}'
M scripts/serve.py +1 -5
M style.css +7 -4