Make the data model use Item and List types universally, with nodes authored as rank-polymorphic kernels (#4335)

* Add rank polymorphism node audit classifying all 271 nodes

* Implement StaticType for Item<T>

* Generate Item and mapped List wire variants for nodes declaring an Item<T> primary input

* Migrate nine nodes to Item element-wise kernels, dissolving the blending trait boilerplate

* Document the Item kernel implementation and staging plan

* Route Item<Vector> through TaggedValue::TypeDefault

* Add executor integration tests covering the Item and List wire variants

* Collapse element-wise Item/List wire pairs to the List form for conversion insertion

* Migrate sixteen vector modifier nodes to Item element-wise kernels

* Migrate Sample Image, Extend Image to Bounds, and Dehaze to Item element-wise kernels

* Fix bevel_with_transform test to actually exercise the transform attribute

* Implement From<T> for Item<T>

* Register PromoteNode rank adapters wrapping bare values into Item wires

* Insert PromoteNode adapters for Item/List wire pair fields in the preprocessor

* Define a real promote node backing the PromoteNode registry identifiers

* Zip ranked Item connectors by frame slot in the mapped element-wise variant

* Register ItemToListNode singleton raise adapters

* Resolve Item wires against List connectors by inserting promotion adapters at construction

* Rank the Offset Points distance connector and prove mixed-rank resolution end-to-end

* Implement Clampable for Item and List wires with per-variant clamp bounds

* Rank the Round Corners radius connector, exercising hard bounds on a ranked wire

* Implement ApplyTransform for Item

* Add Item wire implementations to the Transform node, keeping rank-0 chains rank 0

* Detect element-wise nodes by lazy primary connectors declaring Output = Item

* Convert Transform to an Item kernel with ranked parameters, delivering the broadcast milestone

* Rename Apply Transform to Bake Transform, baking item transforms on Vector, DAffine2, and DVec2

* Promote bare wires onto Item connectors at resolution via WrapItemNode adapters

* Rank the numeric, vector, and boolean parameters across the migrated element-wise nodes

* Rank the enum, integer, and seed parameters, registering their rank adapters via a consolidated macro

* Amend the audit with the DashPattern value type resolution

* Migrate the string family to Item element-wise kernels

* Unwrap Item wires into bare legacy connectors at resolution via UnwrapItemNode adapters

* Shadow owned node parameters in bodies instead of mut in signatures

* Migrate the math family and string measure nodes to Item element-wise kernels

* Convert the comparison and clamp nodes to Item kernels, dropping unreachable &str rows

* Flat-map expander kernels returning List under the mapped variant's frame

* Migrate the expander nodes to Item kernels flat-mapping under the frame

* Remove the unused peel_list helper

* Rank the raster adjustment and blending kernels, recontextualizing shader nodes onto an Item stand-in

Migrate the 16 adjustment nodes, Mix, Color Overlay, and Gradient Map from whole-List kernels to rank-0 Item kernels, letting the macro derive the List-mapped (zip) variants. Move the Adjust and Blend per-element seams off List onto the element types (add the Raster<CPU> impls, drop the now-dead List impls).

Shader nodes keep their bodies verbatim: PerPixelAdjust re-emits the identical kernel against a transparent no_std Item stand-in, so every Item<T> connector and .element() call resolves to a zero-cost identity on the GPU while the uniform buffer stays bare repr(C). The macro peels Item off ranked uniform params, wraps the fetched texel and uniforms at the entry point, and unwraps the result. This drops the shader_node/Item incompatibility guard. Register rank adapters for the adjustment enums.

* Update the rank polymorphism roadmap for the landed shader-node and adjustments chunk

* Rename the GPU Item stand-in to ShaderItem, aliased as Item at its shader-node import sites

* Flip the vector shape generators to emit rank-0 Item<Vector>

The shape generators (Rectangle, Circle, Ellipse, Arc, Spiral, Polygon, Star, Arrow, Line, Grid, QR Code) each produced exactly one shape wrapped in a singleton List<Vector>. Emit Item<Vector> directly so they connect to the rank-0 content connector of the migrated Transform node. Downstream List consumers receive the value through the existing Item to List promotion.

Relax the element-wise validation so a `()` (generator) primary may return Item<T> without being element-wise. Adapt the Repeat on Points test, which still takes a List content connector, by raising the generator's Item output through a singleton wrapper node.

* Parse ranked Item<T> parameter defaults against the bare element type

A ranked `Item<T>` parameter's default value is a bare, unranked `T` (promoted to the wire at resolution), but the preprocessor was handed the wrapped `Item<T>` type and could not parse the literal, flooding the console with warnings and dropping the defaults. Key the field's default_type metadata off the peeled element type for concrete ranked parameters, leaving generic `Item<T>` primaries and skip_impl nodes untouched.

* Parse an element-wise primary's scalar default against the bare element type

An element-wise node's primary reports its default_type as the List wire form so an unconnected primary defaults to an empty list. But when the primary carries a scalar `#[default]` (such as Root's radicand), that literal must parse as a bare element, not a List. Key the primary's default_type off the bare element type when it has a Default value source, keeping the List form otherwise.

* Add the DashPattern value type for stroke dash sequences

Introduce a rank-0 DashPattern value type (a Vec<f64> of alternating dash and gap lengths) so a stroke's dash pattern is a single frameable value rather than a rank-1 List<f64>. Register it as an auto-generated TaggedValue variant, parse its default from a comma or space separated string, and register its rank adapters. Not yet wired into the Stroke node.

* Rank the Fill and Stroke nodes element-wise and give Stroke a DashPattern connector

Migrate Fill and Stroke to element-wise Item<V> primaries (over Vector and Graphic element types) via a new element-level VectorItemMut trait, so styling one shape yields one shape and rank is preserved instead of promoting the input to a singleton List and emitting a List. The macro derives the List-mapped variant for genuine collections.

Wire the Stroke dash sequence to the new rank-0 DashPattern value type, collapsing the old content x paint x dash cartesian and dropping the IntoF64Vec trait. Update the stroke properties dash widget, the drawing tool, and graph-operation plumbing to read and write DashPattern, and migrate legacy F64Array, F64, and String dash inputs on document open.

Assign Colors stays a whole-collection node: each element's gradient position depends on its index among all siblings, which the element frame does not expose, so it keeps its List primary and the VectorListIterMut trait.

* Register rank adapters for the ranked Stroke enum parameters

The element-wise Stroke node ranks its align, cap, and paint order parameters as Item<StrokeAlign>, Item<StrokeCap>, and Item<PaintOrder>, but those enums lacked promotion adapters, so a bare default enum value could not be promoted to its Item wire and no Stroke variant resolved ("No construct found for node"). Register their rank adapters alongside StrokeJoin.

* Display Item wires in the Data panel without a List's ID column

Add a TableItemLayout impl for Item<T> and recognize Item wire types when introspecting graph data. An Item holds a single element, so it renders as a one-row table of the element plus its attributes with no leading index column, and it labels as its element type T rather than a List's T[]. Add ItemAttributeValues::get_any for the attribute widget dispatch.

* Register MonitorNode for Item wire types so the Data panel introspects them directly

Graph introspection wraps the inspected output in a generic MonitorNode typed to the wire. Without Item<T> monitor registrations, an Item<Vector> output could only be monitored after an Item to List promotion, so the Data panel captured and displayed a List<Vector> despite the connector being Item<Vector>. Register monitors for the Item types the element-wise nodes emit, and add the matching Data panel downcast entries.

* Color and double Item/List wires and cleave layer-stack connectors in the node graph

* Route wire color and rank through hidden nodes and refresh them on type changes

* Rework the DashPattern connector conversions with element-wise promotion and an explicit reducer node

* Rank the remaining value, context, aggregation, and transform nodes onto Item<T> wires

* Back DashPattern with a List<f64> so the Data panel can introspect its lengths

* Carry a single Item<T> through varargs so the Read context nodes emit Item<T> not List<T>

* Relax rank validation for aggregation shapes, add element adapters, and match variants by fewest promotions

* Rank the remaining bare and unnecessarily-List connectors across the node catalog

* Add Graphic::None and the FillChoice paint value, making colors and gradients plain values

* Rename GradientStops to Gradient and the legacy Gradient/Fill structs to LegacyGradient/LegacyFill

* Restore generator frame-from-params ranking to the roadmap as a planned stage

* Rename the ranked-field adapter identifier from PromoteNode to FieldAdapterNode to reflect its full contract

* Unload only the wires whose displayed style changed when types update

* Peel wire rank in the editor's semantic type checks so rank-0 layers are recognized

* Restore the whole-List Transform variant so rank-1 content wires resolve again

* Register the Item wire forms for the Memoize and Context Modification infrastructure nodes

* Give every ranked connector a field adapter and add numeric cast variants for legacy wires

* Key a ranked param's type default off its Item wire form when no literal default exists

* Inherit the layer's content value when splicing a node into an empty chain

* Migrate stale List-form TypeDefault inputs to the definition's current default

* Generate the mapped wire variant only when the element-wise node has a frame source

* Let a bare wire feed a List connector via a wrap-raise adapter, costed as two rank steps

* Add a zip companion to the whole-List Transform so ranked List parameters pair per slot

* Add the Sum, Average, Minimum, Maximum, Any, and All list reducers

* Convert the measure family to element-wise Item kernels per the audit classification

* Prefer the bare element value over the Item type default so ranked params keep their widgets

* Rename GradientStopsUI to GradientUI

* Split Fill's optional transform into a _has_transform bool and a ranked _transform matrix

* Rename the migration-only OptionalDAffine2 TaggedValue to LegacyOptionalDAffine2

* Flow byte buffers as Item<Resource> instead of List<u8> across the byte nodes

* Macro-generate the list-content wire variant, retiring the hand-written Transform-zip, Area, and Centroid companions

* Let ()-primary generators take ranked params and frame over them via the mapped variant, ranking Circle's radius

* Rank the vector shape generators' params to Item, adding a rank-aware input grab to the introspection harness

* Rank the value, color, and text generator params to Item

* Rank the raster, web-request, and context-reader generator params to Item

* Fix the repeat and brush test wirings left behind by the param-ranking sweeps

* Delete the vestigial Some, Unwrap Option, and Size Of debug nodes

* Delete the Attach Attribute node, folding its role into Write Attribute

* Add the Filter and Sort list companion nodes

* Guard the removed-definition migration swap target with a test

* Add the Box Corners value type in place of the rectangle corner radius list

* Split Text to Vector's per-glyph mode into a Text to Vector Glyphs node

* Rank the Combine Channels node's channel connectors to Item

* Make Map Points an element-wise node

* Delete the deprecated Upload Texture node

* Update the implementation roadmap to reflect the landed stages

* Let monitor introspection read rank-0 wires, locking in the layer coercion promotion path

* Prefer the rank-0 default when disconnecting a rank-capable input

* Make Path Modify an element-wise node

* Wrap node paths in a NodeIdPath newtype so they flow as a single Item

* Give Item<Raster<CPU>> a default so an unconnected Brush background resolves

* Stop the Brush node from setting layer attributes its paint operation doesn't produce

* Present-gate Flatten Path's adopted layer path like its fill and stroke

* Gate carried layer attributes on static column presence, not runtime values

* Give the remaining graphic Item<T> types a default so unconnected primaries resolve

* Dispatch a ranked param's Properties widget from its rank-0 element type

* Make Extract Transform an element-wise node, restoring the Origins to Polyline body

* Rename Flatten Path to Combine Paths

* Stamp Legacy Layer Extend's adopted layer path as a readable NodeIdPath

* Drop the dead List<u8> and List<NodeId> wire rows

* Rank Flatten Graphic's Fully Flatten toggle to Item

* Update the implementation roadmap with the endgame scope

* Make Combine Paths a reducer that collapses the whole frame into one path

* Stop type-converter nodes from carrying the source's unrelated attributes

* Format the Origins to Polyline regression test

* Wrap the Brush node's trace in a BrushTrace newtype so it flows as one value

* Make Switch a framed element-wise select, bundling whole collections

* Widen and align element-type coverage across the list and graphic nodes

* Register the compiler's cache chain pair for every ranked enum and newtype wire

* Fix wire colors for Passthrough outputs, bundled lists, and bools, and widen list wires

* Represent List wire types structurally with Type::List, replacing name-parsed rank promotion

* Treat scope and data fields as environment, rank scope wires as Item, and feed the render boundary through a context vararg

* Delete the vestigial Clone debug node

* Reinstate Upload Texture as an element-wise node and fix the GPU variants' scope executor and rank adapters

* Rename Combine Paths back to Flatten Path, deferring that rename to its own PR

* Deduplicate the promotion adapter registrations into the field adapter macro

* Rank Write Attribute's value connector to Item<AttributeValueDyn>, retiring the UnwrapItem bridge

* Vertical wire styling

* Store the editor layer path attribute as a bare NodeIdPath, not an Item<NodeIdPath>

* Rank Context Modification's features connector to Item<ContextFeatures>, dropping the dead memoize row

* Rank Path Modify's modification parameter to Item<Box<VectorModification>>

* Rename the field adapter node family to input adapter

* Drop the dead bare scalar rows from Context Modification's implementations list

* Move the dynamic executor's test module into its own file

* Drop the registry's unreachable bare rows for Memoize, the cache chain, and ConvertNode

* Materialize stored TaggedValues as ranked Item wires at the source

* Remove the bare-wire promotion and adapter machinery made dead by ranked value materialization

* Plant the input adapter for List-only inputs, composing position conversion from standard rows

* Consolidate Into/Convert conversions into the input adapter umbrella and rename the rank adapter identifiers

* Fix grouped layers gaining a phantom None stack element from the FillChoice default hijacking every List<Graphic> disconnect

* Enforce ranked node inputs in the macro, rejecting bare wire declarations

* Remove the unit Context => () machinery rows, leaving () purely as the no-primary sentinel

* Add a --signatures rank-audit mode to node-docs for the ranked-wire migration

* Remove the node-docs --signatures rank-audit mode now that ranked wires are enforced

* Migrate legacy no-color values on the Black & White, Color Overlay, and Empty Image color inputs

* Rewrite the element-wise accessor wire type at the primary input, not raw index 0

* Register the cache chain for Resource wires, replacing the lone hand-written Monitor row

* Gate the remaining Raster<GPU> registry rows behind the gpu feature

* Let List<DVec2> wires erase to ListDyn for the attribute reader and element counter

* Rename Extract Element to Item at Index, Count Elements to List Length, and Omit Element to Remove at Index

* Store paint picks as plain color/gradient values, removing the FillChoice value type

* Code review restructuring

* Sort by the consumed sort_key attribute or natural element order, adding the Sort Key node

* Remove the new list-combinator and reducer nodes to defer them to a follow-up PR

* Parse Fill and Stroke color defaults through the paint wire's Graphic element

* Emit ranked implementation-row default types structurally so their element TypeIds survive to default-literal parsing

* Exempt the deliberate no-paint choice from the stale List-form TypeDefault migration

* Migrate the legacy 4-input Fill directly to the split has-transform shape

* Upgrade the demo artwork

* Fix the valid AI review findings: Item eq/hash contract, table-era no-paint migration, quantize List rows, and other smaller issues

* Remove the rank polymorphism working documents

* Hash Item attribute values directly instead of debug-formatting them, speeding up cached evaluation

* Replace the data panel's dead bare-wire downcast arms with full coverage of the ranked monitor row types

* Derive PartialEq for Item now that attributes participate in equality

* Extend the data panel's attribute dispatchers with the newly supported scalar and choice enum types

* Add List monitor rows for the framed numeric conversion outputs so inspecting them resolves, with matching data panel arms
This commit is contained in:
Keavon Chambers
2026-09-08 16:03:01 +00:00
committed by Timon
parent 296185b7fc
commit a708a54492
3257 changed files with 766342 additions and 1829 deletions
@@ -0,0 +1,35 @@
[package]
name = "dyn-any"
version = "0.3.1"
rust-version = "1.85"
edition = "2024"
authors = ["Graphite Authors <contact@graphite.art>"]
description = "An Any trait that works for arbitrary lifetimes"
license = "MIT OR Apache-2.0"
readme = "./README.md"
homepage = "https://crates.io/crates/dyn-any"
repository = "https://github.com/GraphiteEditor/Graphite/tree/master/libraries/dyn-any"
documentation = "https://docs.rs/dyn-any"
[features]
default = ["std", "large-atomics"]
std = ["alloc", "rc", "glam/default"]
large-atomics = []
alloc = []
derive = ["dyn-any-derive"]
log-bad-types = []
# Opt into impls for Rc<T> and Arc<T>.
rc = []
# Opt into impls for some glam types
glam = ["dep:glam"]
[dependencies]
# Optional local dependencies
dyn-any-derive = { path = "derive", optional = true }
# Optional dependencies
glam = { version = "0.32.1", optional = true, default-features = false }
reqwest = { version = "0.13", optional = true, default-features = false }
[package.metadata.docs.rs]
all-features = true
@@ -0,0 +1,201 @@
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.
@@ -0,0 +1,17 @@
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.
@@ -0,0 +1,4 @@
# DynAny Trait
A `Any`-like trait and derive macros for non `'static` lifetimes.
@@ -0,0 +1,24 @@
[package]
name = "dyn-any-derive"
version = "0.3.0"
edition = "2024"
authors = ["Graphite Authors <contact@graphite.art>"]
description = "#[derive(DynAny)]"
documentation = "https://docs.rs/dyn-any-derive"
repository = "https://github.com/GraphiteEditor/Graphite/tree/master/libraries/dyn-any/derive"
license = "MIT OR Apache-2.0"
readme = "../README.md"
[lib]
proc-macro = true
[dependencies]
# Workspace dependencies
proc-macro2 = { workspace = true }
quote = { workspace = true }
syn = { workspace = true }
[dev-dependencies]
# Local dependencies
dyn-any = { path = "..", features = ["derive"] }
@@ -0,0 +1,84 @@
#![doc(html_root_url = "http://docs.rs/dyn-any-derive/0.1.0")]
extern crate proc_macro;
use proc_macro::TokenStream;
use proc_macro2::Span;
use quote::quote;
use syn::{DeriveInput, GenericParam, Lifetime, TypeParamBound, parse_macro_input};
/// Derives an implementation for the [`DynAny`] trait.
///
/// # Note
///
/// Currently only works with `struct` inputs.
///
/// # Example
///
/// ## Struct
///
/// ```
/// # use dyn_any::{DynAny, StaticType};
/// #[derive(DynAny)]
/// pub struct Color<'a, 'b> {
/// r: &'a u8,
/// g: &'b u8,
/// b: &'a u8,
/// }
///
///
/// // Generated Impl
///
/// // impl<'dyn_any> StaticType for Color<'dyn_any, 'dyn_any> {
/// // type Static = Color<'static, 'static>;
/// // }
///
/// ```
#[proc_macro_derive(DynAny, attributes(dyn_any_derive))]
pub fn system_desc_derive(input: TokenStream) -> TokenStream {
let ast = parse_macro_input!(input as DeriveInput);
let struct_name = &ast.ident;
let generics = &ast.generics;
let static_params = generic_arguments(generics, "'static");
let dyn_params = generic_arguments(generics, "'dyn_any");
let impl_params = generics.params.iter().map(|param| match param {
GenericParam::Type(t) => {
let mut t = t.clone();
t.bounds.push(TypeParamBound::Lifetime(Lifetime::new("'static", Span::call_site())));
quote! {#t}
}
param => quote! {#param},
});
quote! {
unsafe impl<'dyn_any, #(#impl_params,)*> dyn_any::StaticType for #struct_name <#(#dyn_params,)*> {
type Static = #struct_name <#(#static_params,)*>;
}
}
.into()
}
/// The struct's generic parameters as argument tokens: bare idents for type
/// and const parameters (bounds are illegal in argument position), the
/// replacement for lifetimes.
fn generic_arguments(generics: &syn::Generics, replacement: &str) -> Vec<proc_macro2::TokenStream> {
generics
.params
.iter()
.map(|param| match param {
GenericParam::Lifetime(_) => {
let lifetime = Lifetime::new(replacement, Span::call_site());
quote! {#lifetime}
}
GenericParam::Type(t) => {
let ident = &t.ident;
quote! {#ident}
}
GenericParam::Const(c) => {
let ident = &c.ident;
quote! {#ident}
}
})
.collect::<Vec<_>>()
}
@@ -0,0 +1,393 @@
#![doc(html_root_url = "http://docs.rs/const-default/1.0.0")]
#![cfg_attr(not(feature = "std"), no_std)]
#![allow(clippy::missing_safety_doc)]
#[cfg(feature = "alloc")]
extern crate alloc;
#[cfg(feature = "derive")]
pub use dyn_any_derive::DynAny;
/// Implement this trait for your `dyn Trait` types for all `T: Trait`
pub trait UpcastFrom<T: ?Sized> {
fn up_from(value: &T) -> &Self;
fn up_from_mut(value: &mut T) -> &mut Self;
#[cfg(feature = "alloc")]
fn up_from_box(value: Box<T>) -> Box<Self>;
}
/// Use this trait to perform your upcasts on dyn traits. Make sure to require it in the supertrait!
pub trait Upcast<U: ?Sized> {
fn up(&self) -> &U;
fn up_mut(&mut self) -> &mut U;
#[cfg(feature = "alloc")]
fn up_box(self: Box<Self>) -> Box<U>;
}
impl<T: ?Sized, U: ?Sized> Upcast<U> for T
where
U: UpcastFrom<T>,
{
fn up(&self) -> &U {
U::up_from(self)
}
fn up_mut(&mut self) -> &mut U {
U::up_from_mut(self)
}
#[cfg(feature = "alloc")]
fn up_box(self: Box<Self>) -> Box<U> {
U::up_from_box(self)
}
}
use core::any::TypeId;
impl<'a, T: DynAny<'a> + 'a> UpcastFrom<T> for dyn DynAny<'a> + 'a {
fn up_from(value: &T) -> &(dyn DynAny<'a> + 'a) {
value
}
fn up_from_mut(value: &mut T) -> &mut (dyn DynAny<'a> + 'a) {
value
}
#[cfg(feature = "alloc")]
fn up_from_box(value: Box<T>) -> Box<Self> {
value
}
}
pub trait DynAny<'a>: 'a {
fn type_id(&self) -> TypeId;
#[cfg(feature = "log-bad-types")]
fn type_name(&self) -> &'static str;
fn reborrow_box<'short>(self: Box<Self>) -> Box<dyn DynAny<'short> + 'short>
where
'a: 'short;
fn reborrow_ref<'short>(&'a self) -> &'short (dyn DynAny<'short> + Send + Sync + 'short)
where
'a: 'short,
Self: Send + Sync;
}
impl<'a, T: StaticType + 'a> DynAny<'a> for T {
fn type_id(&self) -> core::any::TypeId {
core::any::TypeId::of::<T::Static>()
}
#[cfg(feature = "log-bad-types")]
fn type_name(&self) -> &'static str {
core::any::type_name::<T>()
}
fn reborrow_box<'short>(self: Box<Self>) -> Box<dyn DynAny<'short> + 'short>
where
'a: 'short,
{
self
}
fn reborrow_ref<'short>(&'a self) -> &'short (dyn DynAny<'short> + Send + Sync + 'short)
where
'a: 'short,
Self: Send + Sync,
{
self
}
}
pub fn downcast_ref<'a, V: StaticType + 'a>(i: &'a dyn DynAny<'a>) -> Option<&'a V> {
if i.type_id() == core::any::TypeId::of::<<V as StaticType>::Static>() {
// SAFETY: caller guarantees that T is the correct type
let ptr = i as *const dyn DynAny<'a> as *const V;
Some(unsafe { &*ptr })
} else {
None
}
}
#[cfg(feature = "alloc")]
pub fn downcast<'a, V: StaticType + 'a>(i: Box<dyn DynAny<'a> + 'a>) -> Result<Box<V>, String> {
let type_id = DynAny::type_id(i.as_ref());
if type_id == core::any::TypeId::of::<<V as StaticType>::Static>() {
// SAFETY: caller guarantees that T is the correct type
let ptr = Box::into_raw(i) as *mut V;
Ok(unsafe { Box::from_raw(ptr) })
} else {
if type_id == core::any::TypeId::of::<&dyn DynAny<'static>>() {
panic!("downcast error: type_id == core::any::TypeId::of::<dyn DynAny<'a>>()");
}
#[cfg(feature = "log-bad-types")]
{
Err(format!("Incorrect type, expected {} but found {}", core::any::type_name::<V>(), DynAny::type_name(i.as_ref())))
}
#[cfg(not(feature = "log-bad-types"))]
{
Err(format!("Incorrect type, expected {}", core::any::type_name::<V>()))
}
}
}
pub unsafe trait StaticType {
type Static: 'static + ?Sized;
fn type_id(&self) -> core::any::TypeId {
core::any::TypeId::of::<Self::Static>()
}
}
pub unsafe trait StaticTypeSized {
type Static: 'static;
fn type_id(&self) -> core::any::TypeId {
core::any::TypeId::of::<<Self as StaticTypeSized>::Static>()
}
}
unsafe impl<T: StaticType + Sized> StaticTypeSized for T
where
T::Static: Sized,
{
type Static = <T as StaticType>::Static;
}
pub unsafe trait StaticTypeClone {
type Static: 'static + Clone;
fn type_id(&self) -> core::any::TypeId {
core::any::TypeId::of::<<Self as StaticTypeClone>::Static>()
}
}
unsafe impl<T: StaticType + Clone> StaticTypeClone for T
where
T::Static: Clone,
{
type Static = <T as StaticType>::Static;
}
macro_rules! impl_type {
($($id:ident$(<$($(($l:lifetime, $s:lifetime)),*|)?$($T:ident),*>)?),*) => {
$(
unsafe impl< $($($T: $crate::StaticTypeSized ,)*)?> $crate::StaticType for $id $(<$($($l,)*)?$($T, )*>)?{
type Static = $id$(<$($($s,)*)?$(<$T as $crate::StaticTypeSized>::Static,)*>)?;
}
)*
};
}
#[cfg(feature = "alloc")]
unsafe impl<T: StaticTypeClone + Clone> StaticType for Cow<'_, T> {
type Static = Cow<'static, <T as StaticTypeClone>::Static>;
}
unsafe impl<T: StaticTypeSized> StaticType for *const [T] {
type Static = *const [<T as StaticTypeSized>::Static];
}
unsafe impl<T: StaticTypeSized> StaticType for *mut [T] {
type Static = *mut [<T as StaticTypeSized>::Static];
}
macro_rules! impl_slice {
($($id:ident),*) => {
$(
unsafe impl<'a, T: StaticTypeSized> StaticType for $id<'a, T> {
type Static = $id<'static, <T as StaticTypeSized>::Static>;
}
)*
};
}
mod slice {
use super::*;
use core::slice::*;
impl_slice!(Iter, IterMut, Chunks, ChunksMut, RChunks, RChunksMut, Windows);
}
#[cfg(feature = "alloc")]
unsafe impl<T: StaticTypeSized> StaticType for Box<dyn Iterator<Item = T> + '_ + Send + Sync> {
type Static = Box<dyn Iterator<Item = <T as StaticTypeSized>::Static> + Send + Sync>;
}
unsafe impl StaticType for &str {
type Static = &'static str;
}
unsafe impl StaticType for () {
type Static = ();
}
unsafe impl<'a, T: 'a + StaticType + ?Sized> StaticType for &'a T {
type Static = &'static <T as StaticType>::Static;
}
unsafe impl<T: StaticTypeSized, const N: usize> StaticType for [T; N] {
type Static = [<T as StaticTypeSized>::Static; N];
}
unsafe impl<T: StaticTypeSized> StaticType for [T] {
type Static = [<T as StaticTypeSized>::Static];
}
unsafe impl StaticType for dyn for<'i> DynAny<'_> + '_ {
type Static = dyn DynAny<'static>;
}
unsafe impl StaticType for dyn for<'i> DynAny<'_> + Send + Sync + '_ {
type Static = dyn DynAny<'static> + Send + Sync;
}
unsafe impl StaticType for dyn for<'i> DynAny<'_> + Send + '_ {
type Static = dyn DynAny<'static> + Sync;
}
unsafe impl<T: StaticTypeSized> StaticType for dyn core::future::Future<Output = T> + Send + Sync + '_ {
type Static = dyn core::future::Future<Output = T::Static> + Send + Sync;
}
unsafe impl<T: StaticTypeSized> StaticType for dyn core::future::Future<Output = T> + Send + '_ {
type Static = dyn core::future::Future<Output = T::Static> + Send;
}
unsafe impl<T: StaticTypeSized> StaticType for dyn core::future::Future<Output = T> + '_ {
type Static = dyn core::future::Future<Output = T::Static>;
}
#[cfg(feature = "alloc")]
pub trait IntoDynAny<'n>: Sized + StaticType + 'n {
fn into_dyn(self) -> Box<dyn DynAny<'n> + 'n> {
Box::new(self)
}
}
#[cfg(feature = "alloc")]
impl<'n, T: StaticType + 'n> IntoDynAny<'n> for T {}
#[cfg(feature = "alloc")]
impl From<()> for Box<dyn DynAny<'static>> {
fn from(_: ()) -> Box<dyn DynAny<'static>> {
Box::new(())
}
}
#[cfg(feature = "alloc")]
use alloc::borrow::Cow;
#[cfg(feature = "alloc")]
use alloc::boxed::Box;
#[cfg(feature = "alloc")]
use alloc::collections::{BTreeMap, BTreeSet, BinaryHeap, LinkedList, VecDeque};
#[cfg(feature = "alloc")]
use alloc::string::String;
#[cfg(feature = "alloc")]
use alloc::vec::Vec;
use core::cell::{Cell, RefCell, UnsafeCell};
use core::iter::Empty;
use core::marker::{PhantomData, PhantomPinned};
use core::mem::{ManuallyDrop, MaybeUninit};
use core::num::Wrapping;
use core::ops::Range;
use core::pin::Pin;
use core::sync::atomic::*;
use core::time::Duration;
impl_type!(
Option<T>, Result<T, E>, Cell<T>, UnsafeCell<T>, RefCell<T>, MaybeUninit<T>,
ManuallyDrop<T>, PhantomData<T>, PhantomPinned, Empty<T>, Range<T>,
Wrapping<T>, Pin<T>, Duration, bool, f32, f64, char,
u8, AtomicU8, u16, AtomicU16, u32, AtomicU32, u64, usize, AtomicUsize,
i8, AtomicI8, i16, AtomicI16, i32, AtomicI32, i64, isize, AtomicIsize,
i128, u128, AtomicBool, AtomicPtr<T>
);
#[cfg(feature = "large-atomics")]
impl_type!(AtomicU64, AtomicI64);
#[cfg(feature = "alloc")]
impl_type!(
Vec<T>, String, BTreeMap<K,V>,BTreeSet<V>, LinkedList<T>, VecDeque<T>,
BinaryHeap<T>
);
#[cfg(feature = "std")]
use std::collections::{HashMap, HashSet};
#[cfg(feature = "std")]
use std::sync::*;
#[cfg(feature = "std")]
impl_type!(Once, Mutex<T>, RwLock<T>, HashSet<T>, HashMap<K, V>);
#[cfg(feature = "rc")]
use std::rc::Rc;
#[cfg(feature = "rc")]
impl_type!(Rc<T>);
#[cfg(all(feature = "rc", feature = "alloc"))]
use std::sync::Arc;
#[cfg(all(feature = "rc", feature = "alloc"))]
unsafe impl<T: StaticType + ?Sized> StaticType for Arc<T> {
type Static = Arc<<T as StaticType>::Static>;
}
#[cfg(feature = "glam")]
use glam::*;
#[cfg(feature = "glam")]
#[rustfmt::skip]
impl_type!(
IVec2, IVec3, IVec4, UVec2, UVec3, UVec4, BVec2, BVec3, BVec4,
Vec2, Vec3, Vec3A, Vec4, DVec2, DVec3, DVec4,
Mat2, Mat3, Mat3A, Mat4, DMat2, DMat3, DMat4,
Quat, Affine2, Affine3A, DAffine2, DAffine3, DQuat
);
#[cfg(feature = "reqwest")]
use reqwest::Response;
#[cfg(feature = "reqwest")]
impl_type!(Response);
#[cfg(feature = "alloc")]
unsafe impl<T: crate::StaticType + ?Sized> crate::StaticType for Box<T> {
type Static = Box<<T as crate::StaticType>::Static>;
}
#[test]
fn test_tuple_of_boxes() {
let tuple = (Box::new(&1u32 as &dyn DynAny<'static>), Box::new(&2u32 as &dyn DynAny<'static>));
let dyn_any = &tuple as &dyn DynAny;
assert_eq!(&1, downcast_ref::<u32>(*downcast_ref::<(Box<&dyn DynAny>, Box<&dyn DynAny>)>(dyn_any).unwrap().0).unwrap());
assert_eq!(&2, downcast_ref::<u32>(*downcast_ref::<(Box<&dyn DynAny>, Box<&dyn DynAny>)>(dyn_any).unwrap().1).unwrap());
}
macro_rules! impl_tuple {
(@rec $t:ident) => { };
(@rec $_:ident $($t:ident)+) => {
impl_tuple! { @impl $($t)* }
impl_tuple! { @rec $($t)* }
};
(@impl $($t:ident)*) => {
unsafe impl< $($t: StaticTypeSized,)*> StaticType for ($($t,)*) {
type Static = ($(<$t as $crate::StaticTypeSized>::Static,)*);
}
};
($($t:ident)*) => {
impl_tuple! { @rec _t $($t)* }
};
}
impl_tuple! {
A B C D E F G H I J K L
}
#[test]
fn simple_downcast() {
let x = Box::new(3_u32) as Box<dyn DynAny>;
assert_eq!(*downcast::<u32>(x).unwrap(), 3_u32);
}
#[test]
#[should_panic]
fn simple_downcast_panic() {
let x = Box::new(3_i32) as Box<dyn DynAny>;
assert_eq!(*downcast::<u32>(x).expect("attempted to perform invalid downcast"), 3_u32);
}
#[cfg(not(target_family = "wasm"))]
pub trait WasmNotSend: Send {}
#[cfg(target_family = "wasm")]
pub trait WasmNotSend {}
#[cfg(not(target_family = "wasm"))]
impl<T: Send> WasmNotSend for T {}
#[cfg(target_family = "wasm")]
impl<T> WasmNotSend for T {}
#[cfg(not(target_family = "wasm"))]
pub trait WasmNotSync: Sync {}
#[cfg(target_family = "wasm")]
pub trait WasmNotSync {}
#[cfg(not(target_family = "wasm"))]
impl<T: Sync> WasmNotSync for T {}
#[cfg(target_family = "wasm")]
impl<T> WasmNotSync for T {}
#[cfg(not(target_family = "wasm"))]
#[cfg(feature = "alloc")]
pub type DynFuture<'n, T> = Pin<Box<dyn core::future::Future<Output = T> + 'n + Send>>;
#[cfg(target_family = "wasm")]
#[cfg(feature = "alloc")]
pub type DynFuture<'n, T> = Pin<Box<dyn core::future::Future<Output = T> + 'n>>;
@@ -0,0 +1,22 @@
[package]
name = "math-parser"
version = "0.0.0"
rust-version = "1.85"
edition = "2024"
authors = ["Graphite Authors <contact@graphite.art>"]
description = "Parser for Graphite style mathematics expressions"
license = "MIT OR Apache-2.0"
[dependencies]
pest = "2.7"
pest_derive = "2.7"
thiserror = "2.0"
lazy_static = "1.5"
num-complex = "0.4"
[dev-dependencies]
criterion = { workspace = true }
[[bench]]
name = "bench"
harness = false
@@ -0,0 +1,50 @@
use std::hint::black_box;
use criterion::{Criterion, criterion_group, criterion_main};
use math_parser::{ast, context::EvalContext};
macro_rules! generate_benchmarks {
($( $input:expr_2021 ),* $(,)?) => {
fn parsing_bench(c: &mut Criterion) {
$(
c.bench_function(concat!("parse ", $input), |b| {
b.iter(|| {
let _ = black_box(ast::Node::try_parse_from_str($input)).unwrap();
});
});
)*
}
fn evaluation_bench(c: &mut Criterion) {
$(
let expr = ast::Node::try_parse_from_str($input).unwrap().0;
let context = EvalContext::default();
c.bench_function(concat!("eval ", $input), |b| {
b.iter(|| {
let _ = black_box(expr.eval(&context));
});
});
)*
}
criterion_group!(benches, parsing_bench, evaluation_bench);
criterion_main!(benches);
};
}
generate_benchmarks! {
"(3 * (4 + sqrt(25)) - cos(pi/3) * (2^3)) + 5 * e", // Mixed nested functions, constants, and operations
"((5 + 2 * (3 - sqrt(49)))^2) / (1 + sqrt(16)) + tau / 2", // Complex nested expression with constants
"log(100, 10) + (5 * sin(pi/4) + sqrt(81)) / (2 * phi)", // Logarithmic and trigonometric functions
"(sqrt(144) * 2 + 5) / (3 * (4 - sin(pi / 6))) + e^2", // Combined square root, trigonometric, and exponential operations
"cos(2 * pi) + tan(pi / 3) * log(32, 2) - sqrt(256)", // Multiple trigonometric and logarithmic functions
"(10 * (3 + 2) - 8 / 2)^2 + 7 * (2^4) - sqrt(225) + phi", // Mixed arithmetic with constants
"(5^2 + 3^3) * (sqrt(81) + sqrt(64)) - tau * log(1000, 10)", // Power and square root with constants
"((8 * sqrt(49) - 2 * e) + log(256, 2) / (2 + cos(pi))) * 1.5", // Nested functions and constants
"(tan(pi / 4) + 5) * (3 + sqrt(36)) / (log(1024, 2) - 4)", // Nested functions with trigonometry and logarithm
"((3 * e + 2 * sqrt(100)) - cos(tau / 4)) * log(27, 3) + phi", // Mixed constant usage and functions
"(sqrt(100) + 5 * sin(pi / 6) - 8 / log(64, 2)) + e^(1.5)", // Complex mix of square root, division, and exponentiation
"((sin(pi/2) + cos(0)) * (e^2 - 2 * sqrt(16))) / (log(100, 10) + pi)", // Nested trigonometric, exponential, and logarithmic functions
"(5 * (7 + sqrt(121)) - (log(243, 3) * phi)) + 3^5 / tau", //
}
@@ -0,0 +1,75 @@
use crate::value::Complex;
#[derive(Debug, PartialEq, Eq)]
pub struct Unit {
// Exponent of length unit (meters)
pub length: i32,
// Exponent of mass unit (kilograms)
pub mass: i32,
// Exponent of time unit (seconds)
pub time: i32,
}
impl Default for Unit {
fn default() -> Self {
Self::BASE_UNIT
}
}
impl Unit {
pub const BASE_UNIT: Unit = Unit { length: 0, mass: 0, time: 0 };
pub const LENGTH: Unit = Unit { length: 1, mass: 0, time: 0 };
pub const MASS: Unit = Unit { length: 0, mass: 1, time: 0 };
pub const TIME: Unit = Unit { length: 0, mass: 0, time: 1 };
pub const VELOCITY: Unit = Unit { length: 1, mass: 0, time: -1 };
pub const ACCELERATION: Unit = Unit { length: 1, mass: 0, time: -2 };
pub const FORCE: Unit = Unit { length: 1, mass: 1, time: -2 };
pub fn base_unit() -> Self {
Self::BASE_UNIT
}
pub fn is_base(&self) -> bool {
*self == Self::BASE_UNIT
}
}
#[derive(Debug, PartialEq)]
pub enum Literal {
Float(f64),
Complex(Complex),
}
impl From<f64> for Literal {
fn from(value: f64) -> Self {
Self::Float(value)
}
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum BinaryOp {
Add,
Sub,
Mul,
Div,
Pow,
}
#[derive(Debug, PartialEq, Clone, Copy)]
pub enum UnaryOp {
Neg,
Sqrt,
Fac,
}
#[derive(Debug, PartialEq)]
pub enum Node {
Lit(Literal),
Var(String),
FnCall { name: String, expr: Vec<Node> },
BinOp { lhs: Box<Node>, op: BinaryOp, rhs: Box<Node> },
UnaryOp { expr: Box<Node>, op: UnaryOp },
}
@@ -0,0 +1,122 @@
use crate::value::{Number, Value};
use lazy_static::lazy_static;
use num_complex::{Complex, ComplexFloat};
use std::collections::HashMap;
use std::f64::consts::PI;
type FunctionImplementation = Box<dyn Fn(&[Value]) -> Option<Value> + Send + Sync>;
lazy_static! {
pub static ref DEFAULT_FUNCTIONS: HashMap<&'static str, FunctionImplementation> = {
let mut map: HashMap<&'static str, FunctionImplementation> = HashMap::new();
map.insert(
"sin",
Box::new(|values| match values {
[Value::Number(Number::Real(real))] => Some(Value::Number(Number::Real(real.sin()))),
[Value::Number(Number::Complex(complex))] => Some(Value::Number(Number::Complex(complex.sin()))),
_ => None,
}),
);
map.insert(
"cos",
Box::new(|values| match values {
[Value::Number(Number::Real(real))] => Some(Value::Number(Number::Real(real.cos()))),
[Value::Number(Number::Complex(complex))] => Some(Value::Number(Number::Complex(complex.cos()))),
_ => None,
}),
);
map.insert(
"tan",
Box::new(|values| match values {
[Value::Number(Number::Real(real))] => Some(Value::Number(Number::Real(real.tan()))),
[Value::Number(Number::Complex(complex))] => Some(Value::Number(Number::Complex(complex.tan()))),
_ => None,
}),
);
map.insert(
"csc",
Box::new(|values| match values {
[Value::Number(Number::Real(real))] => Some(Value::Number(Number::Real(real.sin().recip()))),
[Value::Number(Number::Complex(complex))] => Some(Value::Number(Number::Complex(complex.sin().recip()))),
_ => None,
}),
);
map.insert(
"sec",
Box::new(|values| match values {
[Value::Number(Number::Real(real))] => Some(Value::Number(Number::Real(real.cos().recip()))),
[Value::Number(Number::Complex(complex))] => Some(Value::Number(Number::Complex(complex.cos().recip()))),
_ => None,
}),
);
map.insert(
"cot",
Box::new(|values| match values {
[Value::Number(Number::Real(real))] => Some(Value::Number(Number::Real(real.tan().recip()))),
[Value::Number(Number::Complex(complex))] => Some(Value::Number(Number::Complex(complex.tan().recip()))),
_ => None,
}),
);
map.insert(
"invsin",
Box::new(|values| match values {
[Value::Number(Number::Real(real))] => Some(Value::Number(Number::Real(real.asin()))),
[Value::Number(Number::Complex(complex))] => Some(Value::Number(Number::Complex(complex.asin()))),
_ => None,
}),
);
map.insert(
"invcos",
Box::new(|values| match values {
[Value::Number(Number::Real(real))] => Some(Value::Number(Number::Real(real.acos()))),
[Value::Number(Number::Complex(complex))] => Some(Value::Number(Number::Complex(complex.acos()))),
_ => None,
}),
);
map.insert(
"invtan",
Box::new(|values| match values {
[Value::Number(Number::Real(real))] => Some(Value::Number(Number::Real(real.atan()))),
[Value::Number(Number::Complex(complex))] => Some(Value::Number(Number::Complex(complex.atan()))),
_ => None,
}),
);
map.insert(
"invcsc",
Box::new(|values| match values {
[Value::Number(Number::Real(real))] => Some(Value::Number(Number::Real(real.recip().asin()))),
[Value::Number(Number::Complex(complex))] => Some(Value::Number(Number::Complex(complex.recip().asin()))),
_ => None,
}),
);
map.insert(
"invsec",
Box::new(|values| match values {
[Value::Number(Number::Real(real))] => Some(Value::Number(Number::Real(real.recip().acos()))),
[Value::Number(Number::Complex(complex))] => Some(Value::Number(Number::Complex(complex.recip().acos()))),
_ => None,
}),
);
map.insert(
"invcot",
Box::new(|values| match values {
[Value::Number(Number::Real(real))] => Some(Value::Number(Number::Real((PI / 2. - real).atan()))),
[Value::Number(Number::Complex(complex))] => Some(Value::Number(Number::Complex((Complex::new(PI / 2., 0.) - complex).atan()))),
_ => None,
}),
);
map
};
}
@@ -0,0 +1,80 @@
use crate::value::Value;
use std::collections::HashMap;
use std::ops::{Deref, DerefMut};
//TODO: editor integration, implement these traits for whatever is needed, maybe merge them if needed
pub trait ValueProvider {
fn get_value(&self, name: &str) -> Option<Value>;
}
pub trait FunctionProvider {
fn run_function(&self, name: &str, args: &[Value]) -> Option<Value>;
}
pub struct ValueMap(HashMap<String, Value>);
pub struct NothingMap;
impl ValueProvider for &ValueMap {
fn get_value(&self, name: &str) -> Option<Value> {
self.0.get(name).cloned()
}
}
impl ValueProvider for NothingMap {
fn get_value(&self, _: &str) -> Option<Value> {
None
}
}
impl ValueProvider for ValueMap {
fn get_value(&self, name: &str) -> Option<Value> {
self.0.get(name).cloned()
}
}
impl Deref for ValueMap {
type Target = HashMap<String, Value>;
fn deref(&self) -> &Self::Target {
&self.0
}
}
impl DerefMut for ValueMap {
fn deref_mut(&mut self) -> &mut Self::Target {
&mut self.0
}
}
impl FunctionProvider for NothingMap {
fn run_function(&self, _: &str, _: &[Value]) -> Option<Value> {
None
}
}
pub struct EvalContext<V: ValueProvider, F: FunctionProvider> {
values: V,
functions: F,
}
impl Default for EvalContext<NothingMap, NothingMap> {
fn default() -> Self {
Self {
values: NothingMap,
functions: NothingMap,
}
}
}
impl<V: ValueProvider, F: FunctionProvider> EvalContext<V, F> {
pub fn new(values: V, functions: F) -> Self {
Self { values, functions }
}
pub fn get_value(&self, name: &str) -> Option<Value> {
self.values.get_value(name)
}
pub fn run_function(&self, name: &str, args: &[Value]) -> Option<Value> {
self.functions.run_function(name, args)
}
}
@@ -0,0 +1,100 @@
use crate::ast::{Literal, Node};
use crate::constants::DEFAULT_FUNCTIONS;
use crate::context::{EvalContext, FunctionProvider, ValueProvider};
use crate::value::{Number, Value};
use thiserror::Error;
#[derive(Debug, Error)]
pub enum EvalError {
#[error("Missing value: {0}")]
MissingValue(String),
#[error("Missing function: {0}")]
MissingFunction(String),
#[error("Wrong type for function call")]
TypeError,
}
impl Node {
pub fn eval<V: ValueProvider, F: FunctionProvider>(&self, context: &EvalContext<V, F>) -> Result<Value, EvalError> {
match self {
Node::Lit(lit) => match lit {
Literal::Float(num) => Ok(Value::from_f64(*num)),
Literal::Complex(num) => Ok(Value::Number(Number::Complex(*num))),
},
Node::BinOp { lhs, op, rhs } => match (lhs.eval(context)?, rhs.eval(context)?) {
(Value::Number(lhs), Value::Number(rhs)) => Ok(Value::Number(lhs.binary_op(*op, rhs))),
},
Node::UnaryOp { expr, op } => match expr.eval(context)? {
Value::Number(num) => Ok(Value::Number(num.unary_op(*op))),
},
Node::Var(name) => context.get_value(name).ok_or_else(|| EvalError::MissingValue(name.clone())),
Node::FnCall { name, expr } => {
let values = expr.iter().map(|expr| expr.eval(context)).collect::<Result<Vec<Value>, EvalError>>()?;
if let Some(function) = DEFAULT_FUNCTIONS.get(&name.as_str()) {
function(&values).ok_or(EvalError::TypeError)
} else if let Some(val) = context.run_function(name, &values) {
Ok(val)
} else {
context.get_value(name).ok_or_else(|| EvalError::MissingFunction(name.to_string()))
}
}
}
}
}
#[cfg(test)]
mod tests {
use crate::ast::{BinaryOp, Literal, Node, UnaryOp};
use crate::context::{EvalContext, ValueMap};
use crate::value::Value;
macro_rules! eval_tests {
($($name:ident: $expected:expr_2021 => $expr:expr_2021),* $(,)?) => {
$(
#[test]
fn $name() {
let result = $expr.eval(&EvalContext::default()).unwrap();
assert_eq!(result, $expected);
}
)*
};
}
eval_tests! {
test_addition: Value::from_f64(7.) => Node::BinOp {
lhs: Box::new(Node::Lit(Literal::Float(3.))),
op: BinaryOp::Add,
rhs: Box::new(Node::Lit(Literal::Float(4.))),
},
test_subtraction: Value::from_f64(1.) => Node::BinOp {
lhs: Box::new(Node::Lit(Literal::Float(5.))),
op: BinaryOp::Sub,
rhs: Box::new(Node::Lit(Literal::Float(4.))),
},
test_multiplication: Value::from_f64(12.) => Node::BinOp {
lhs: Box::new(Node::Lit(Literal::Float(3.))),
op: BinaryOp::Mul,
rhs: Box::new(Node::Lit(Literal::Float(4.))),
},
test_division: Value::from_f64(2.5) => Node::BinOp {
lhs: Box::new(Node::Lit(Literal::Float(5.))),
op: BinaryOp::Div,
rhs: Box::new(Node::Lit(Literal::Float(2.))),
},
test_negation: Value::from_f64(-3.) => Node::UnaryOp {
expr: Box::new(Node::Lit(Literal::Float(3.))),
op: UnaryOp::Neg,
},
test_sqrt: Value::from_f64(2.) => Node::UnaryOp {
expr: Box::new(Node::Lit(Literal::Float(4.))),
op: UnaryOp::Sqrt,
},
test_power: Value::from_f64(8.) => Node::BinOp {
lhs: Box::new(Node::Lit(Literal::Float(2.))),
op: BinaryOp::Pow,
rhs: Box::new(Node::Lit(Literal::Float(3.))),
},
}
}
@@ -0,0 +1,60 @@
WHITESPACE = _{ " " | "\t" }
// TODO: Proper indentation and formatting
program = _{ SOI ~ expr ~ EOI }
expr = { atom ~ (infix ~ atom)* }
atom = _{ prefix? ~ primary ~ postfix? }
infix = _{ add | sub | mul | div | pow | paren }
add = { "+" } // Addition
sub = { "-" } // Subtraction
mul = { "*" } // Multiplication
div = { "/" } // Division
mod = { "%" } // Modulo
pow = { "^" } // Exponentiation
paren = { "" } // Implicit multiplication operator
prefix = _{ neg | sqrt }
neg = { "-" } // Negation
sqrt = { "sqrt" }
postfix = _{ fac }
fac = { "!" } // Factorial
primary = _{ ("(" ~ expr ~ ")") | lit | constant | fn_call | ident }
fn_call = { ident ~ "(" ~ expr ~ ("," ~ expr)* ~ ")" }
ident = @{ (ASCII_ALPHA | "_") ~ (ASCII_ALPHANUMERIC | "_")* }
lit = { unit | ((float | int) ~ unit?) }
float = @{ (int ~ "." ~ int? ~ exp? | "." ~ int ~ exp? | int ~ exp) ~ !("." | ASCII_DIGIT) }
exp = _{ ^"e" ~ ("+" | "-")? ~ int }
int = @{ ASCII_DIGIT+ }
unit = ${ (scale ~ base_unit) | base_unit ~ !ident}
base_unit = _{ meter | second | gram }
meter = { "m" }
second = { "s" }
gram = { "g" }
scale = _{ nano | micro | milli | centi | deci | deca | hecto | kilo | mega | giga | tera }
nano = { "n" }
micro = { "µ" | "u" }
milli = { "m" }
centi = { "c" }
deci = { "d" }
deca = { "da" }
hecto = { "h" }
kilo = { "k" }
mega = { "M" }
giga = { "G" }
tera = { "T" }
// Constants
constant = { infinity | imaginary_unit | pi | tau | euler_number | golden_ratio | gravity_acceleration }
infinity = { "inf" | "INF" | "infinity" | "INFINITY" | "∞" }
imaginary_unit = { "i" | "I" }
pi = { "pi" | "PI" | "π" }
tau = { "tau" | "TAU" | "τ" }
euler_number = { "e" }
golden_ratio = { "phi" | "PHI" | "φ" }
gravity_acceleration = { "G" }
@@ -0,0 +1,162 @@
#![allow(unused)]
pub mod ast;
mod constants;
pub mod context;
pub mod executer;
pub mod parser;
pub mod value;
use ast::Unit;
use context::{EvalContext, ValueMap};
use executer::EvalError;
use parser::ParseError;
use value::Value;
pub fn evaluate(expression: &str) -> Result<(Result<Value, EvalError>, Unit), ParseError> {
let expr = ast::Node::try_parse_from_str(expression);
let context = EvalContext::default();
expr.map(|(node, unit)| (node.eval(&context), unit))
}
#[cfg(test)]
mod tests {
use super::*;
use ast::Unit;
use value::Number;
const EPSILON: f64 = 1e-10_f64;
#[test]
fn malformed_juxtaposed_numbers_fail_to_parse() {
// Two numbers cannot be glued together by a stray decimal point (they must not parse as implicit multiplication).
for input in ["1..5", "1.5.5", "1..", ".5.5"] {
assert!(evaluate(input).is_err(), "expected `{input}` to be a parse error");
}
}
macro_rules! test_end_to_end{
($($name:ident: $input:expr_2021 => ($expected_value:expr_2021, $expected_unit:expr_2021)),* $(,)?) => {
$(
#[test]
fn $name() {
let expected_value = $expected_value;
let expected_unit = $expected_unit;
let expr = ast::Node::try_parse_from_str($input);
let context = EvalContext::default();
let (actual_value, actual_unit) = expr.map(|(node, unit)| (node.eval(&context), unit)).unwrap();
let actual_value = actual_value.unwrap();
assert!(actual_unit == expected_unit, "Expected unit {:?} but found unit {:?}", expected_unit, actual_unit);
let expected_value = expected_value.into();
match (actual_value, expected_value) {
(Value::Number(Number::Complex(actual_c)), Value::Number(Number::Complex(expected_c))) => {
assert!(
(actual_c.re.is_infinite() && expected_c.re.is_infinite()) || (actual_c.re - expected_c.re).abs() < EPSILON,
"Expected real part {}, but got {}",
expected_c.re,
actual_c.re
);
assert!(
(actual_c.im.is_infinite() && expected_c.im.is_infinite()) || (actual_c.im - expected_c.im).abs() < EPSILON,
"Expected imaginary part {}, but got {}",
expected_c.im,
actual_c.im
);
}
(Value::Number(Number::Real(actual_f)), Value::Number(Number::Real(expected_f))) => {
if actual_f.is_infinite() || expected_f.is_infinite() {
assert!(
actual_f.is_infinite() && expected_f.is_infinite() && actual_f == expected_f,
"Expected infinite value {}, but got {}",
expected_f,
actual_f
);
} else if actual_f.is_nan() || expected_f.is_nan() {
assert!(actual_f.is_nan() && expected_f.is_nan(), "Expected NaN, but got {}", actual_f);
} else {
assert!((actual_f - expected_f).abs() < EPSILON, "Expected {}, but got {}", expected_f, actual_f);
}
}
// Handle mismatched types
_ => panic!("Mismatched types: expected {:?}, got {:?}", expected_value, actual_value),
}
}
)*
};
}
test_end_to_end! {
// Basic arithmetic and units
infix_addition: "5 + 5" => (10., Unit::BASE_UNIT),
infix_subtraction_units: "5m - 3m" => (2., Unit::LENGTH),
infix_multiplication_units: "4s * 4s" => (16., Unit { length: 0, mass: 0, time: 2 }),
infix_division_units: "8m/2s" => (4., Unit::VELOCITY),
// Order of operations
order_of_operations_negative_prefix: "-10 + 5" => (-5., Unit::BASE_UNIT),
order_of_operations_add_multiply: "5+1*1+5" => (11., Unit::BASE_UNIT),
order_of_operations_add_negative_multiply: "5+(-1)*1+5" => (9., Unit::BASE_UNIT),
order_of_operations_sqrt: "sqrt25 + 11" => (16., Unit::BASE_UNIT),
order_of_operations_sqrt_expression: "sqrt(25+11)" => (6., Unit::BASE_UNIT),
// Parentheses and nested expressions
parentheses_nested_multiply: "(5 + 3) * (2 + 6)" => (64., Unit::BASE_UNIT),
parentheses_mixed_operations: "2 * (3 + 5 * (2 + 1))" => (36., Unit::BASE_UNIT),
parentheses_divide_add_multiply: "10 / (2 + 3) + (7 * 2)" => (16., Unit::BASE_UNIT),
// Square root and nested square root
sqrt_chain_operations: "sqrt(16) + sqrt(9) * sqrt(4)" => (10., Unit::BASE_UNIT),
sqrt_nested: "sqrt(sqrt(81))" => (3., Unit::BASE_UNIT),
sqrt_divide_expression: "sqrt((25 + 11) / 9)" => (2., Unit::BASE_UNIT),
// Mixed square root and units
sqrt_multiply_units: "sqrt(16) * 2g + 5g" => (13., Unit::MASS),
sqrt_add_multiply: "sqrt(49) - 1 + 2 * 3" => (12., Unit::BASE_UNIT),
sqrt_addition_multiply: "(sqrt(36) + 2) * 2" => (16., Unit::BASE_UNIT),
// Exponentiation
exponent_single: "2^3" => (8., Unit::BASE_UNIT),
exponent_mixed_operations: "2^3 + 4^2" => (24., Unit::BASE_UNIT),
exponent_nested: "2^(3+1)" => (16., Unit::BASE_UNIT),
// Operations with negative values
negative_units_add_multiply: "-5s + (-3 * 2)s" => (-11., Unit::TIME),
negative_nested_parentheses: "-(5 + 3 * (2 - 1))" => (-8., Unit::BASE_UNIT),
negative_sqrt_addition: "-(sqrt(16) + sqrt(9))" => (-7., Unit::BASE_UNIT),
multiply_sqrt_subtract: "5 * 2 + sqrt(16) / 2 - 3" => (9., Unit::BASE_UNIT),
add_multiply_subtract_sqrt: "4 + 3 * (2 + 1) - sqrt(25)" => (8., Unit::BASE_UNIT),
add_sqrt_subtract_nested_multiply: "10 + sqrt(64) - (5 * (2 + 1))" => (3., Unit::BASE_UNIT),
// Mathematical constants
constant_pi: "pi" => (std::f64::consts::PI, Unit::BASE_UNIT),
constant_e: "e" => (std::f64::consts::E, Unit::BASE_UNIT),
constant_phi: "phi" => (1.61803398875, Unit::BASE_UNIT),
constant_tau: "tau" => (2. * std::f64::consts::PI, Unit::BASE_UNIT),
constant_infinity: "inf" => (f64::INFINITY, Unit::BASE_UNIT),
constant_infinity_symbol: "∞" => (f64::INFINITY, Unit::BASE_UNIT),
multiply_pi: "2 * pi" => (2. * std::f64::consts::PI, Unit::BASE_UNIT),
add_e_constant: "e + 1" => (std::f64::consts::E + 1., Unit::BASE_UNIT),
multiply_phi_constant: "phi * 2" => (1.61803398875 * 2., Unit::BASE_UNIT),
exponent_tau: "2^tau" => (2f64.powf(2. * std::f64::consts::PI), Unit::BASE_UNIT),
infinity_subtract_large_number: "inf - 1000" => (f64::INFINITY, Unit::BASE_UNIT),
// Decimals with no leading digit before the point
leading_dot_decimal: ".5" => (0.5, Unit::BASE_UNIT),
leading_dot_in_expression: "1+.5" => (1.5, Unit::BASE_UNIT),
leading_dot_exponent: ".5e3" => (500., Unit::BASE_UNIT),
// Trigonometric functions
trig_sin_pi: "sin(pi)" => (0., Unit::BASE_UNIT),
trig_cos_zero: "cos(0)" => (1., Unit::BASE_UNIT),
trig_tan_pi_div_four: "tan(pi/4)" => (1., Unit::BASE_UNIT),
trig_sin_tau: "sin(tau)" => (0., Unit::BASE_UNIT),
trig_cos_tau_div_two: "cos(tau/2)" => (-1., Unit::BASE_UNIT),
}
}
@@ -0,0 +1,379 @@
use crate::ast::{BinaryOp, Literal, Node, UnaryOp, Unit};
use crate::context::EvalContext;
use crate::value::{Complex, Number, Value};
use lazy_static::lazy_static;
use num_complex::ComplexFloat;
use pest::Parser;
use pest::iterators::{Pair, Pairs};
use pest::pratt_parser::{Assoc, Op, PrattParser};
use pest_derive::Parser;
use std::num::{ParseFloatError, ParseIntError};
use thiserror::Error;
#[derive(Parser)]
#[grammar = "./grammer.pest"] // Point to the grammar file
struct ExprParser;
lazy_static! {
static ref PRATT_PARSER: PrattParser<Rule> = {
PrattParser::new()
.op(Op::infix(Rule::add, Assoc::Left) | Op::infix(Rule::sub, Assoc::Left))
.op(Op::infix(Rule::mul, Assoc::Left) | Op::infix(Rule::div, Assoc::Left) | Op::infix(Rule::paren, Assoc::Left))
.op(Op::infix(Rule::pow, Assoc::Right))
.op(Op::postfix(Rule::fac) | Op::postfix(Rule::EOI))
.op(Op::prefix(Rule::sqrt))
.op(Op::prefix(Rule::neg))
};
}
#[derive(Error, Debug)]
pub enum TypeError {
#[error("Invalid BinOp: {0:?} {1:?} {2:?}")]
InvalidBinaryOp(Unit, BinaryOp, Unit),
#[error("Invalid UnaryOp: {0:?}")]
InvalidUnaryOp(Unit, UnaryOp),
}
#[derive(Error, Debug)]
pub enum ParseError {
#[error("ParseIntError: {0}")]
ParseInt(#[from] ParseIntError),
#[error("ParseFloatError: {0}")]
ParseFloat(#[from] ParseFloatError),
#[error("TypeError: {0}")]
Type(#[from] TypeError),
#[error("PestError: {0}")]
Pest(#[from] Box<pest::error::Error<Rule>>),
}
impl Node {
pub fn try_parse_from_str(s: &str) -> Result<(Node, Unit), ParseError> {
let pairs = ExprParser::parse(Rule::program, s).map_err(Box::new)?;
let (node, metadata) = parse_expr(pairs)?;
Ok((node, metadata.unit))
}
}
struct NodeMetadata {
pub unit: Unit,
}
impl NodeMetadata {
pub fn new(unit: Unit) -> Self {
Self { unit }
}
}
fn parse_unit(pairs: Pairs<Rule>) -> Result<(Unit, f64), ParseError> {
let mut scale = 1.;
let mut length = 0;
let mut mass = 0;
let mut time = 0;
for pair in pairs {
println!("found rule: {:?}", pair.as_rule());
match pair.as_rule() {
Rule::nano => scale *= 1e-9,
Rule::micro => scale *= 1e-6,
Rule::milli => scale *= 1e-3,
Rule::centi => scale *= 1e-2,
Rule::deci => scale *= 1e-1,
Rule::deca => scale *= 1e1,
Rule::hecto => scale *= 1e2,
Rule::kilo => scale *= 1e3,
Rule::mega => scale *= 1e6,
Rule::giga => scale *= 1e9,
Rule::tera => scale *= 1e12,
Rule::meter => length = 1,
Rule::gram => mass = 1,
Rule::second => time = 1,
_ => unreachable!(), // All possible rules should be covered
}
}
Ok((Unit { length, mass, time }, scale))
}
fn parse_const(pair: Pair<Rule>) -> Literal {
match pair.as_rule() {
Rule::infinity => Literal::Float(f64::INFINITY),
Rule::imaginary_unit => Literal::Complex(Complex::new(0., 1.)),
Rule::pi => Literal::Float(std::f64::consts::PI),
Rule::tau => Literal::Float(2. * std::f64::consts::PI),
Rule::euler_number => Literal::Float(std::f64::consts::E),
Rule::golden_ratio => Literal::Float(1.61803398875),
_ => unreachable!("Unexpected constant: {:?}", pair),
}
}
fn parse_lit(mut pairs: Pairs<Rule>) -> Result<(Literal, Unit), ParseError> {
let literal = match pairs.next() {
Some(lit) => match lit.as_rule() {
Rule::int => {
let value = lit.as_str().parse::<i32>()? as f64;
Literal::Float(value)
}
Rule::float => {
let value = lit.as_str().parse::<f64>()?;
Literal::Float(value)
}
Rule::unit => {
let (unit, scale) = parse_unit(lit.into_inner())?;
return Ok((Literal::Float(scale), unit));
}
rule => unreachable!("unexpected rule: {:?}", rule),
},
None => unreachable!("expected rule"), // No literal found
};
if let Some(unit_pair) = pairs.next() {
let unit_pairs = unit_pair.into_inner(); // Get the inner pairs for the unit
let (unit, scale) = parse_unit(unit_pairs)?;
println!("found unit: {unit:?}");
Ok((
match literal {
Literal::Float(num) => Literal::Float(num * scale),
Literal::Complex(num) => Literal::Complex(num * scale),
},
unit,
))
} else {
Ok((literal, Unit::BASE_UNIT))
}
}
fn parse_expr(pairs: Pairs<Rule>) -> Result<(Node, NodeMetadata), ParseError> {
PRATT_PARSER
.map_primary(|primary| {
Ok(match primary.as_rule() {
Rule::lit => {
let (lit, unit) = parse_lit(primary.into_inner())?;
(Node::Lit(lit), NodeMetadata { unit })
}
Rule::fn_call => {
let mut pairs = primary.into_inner();
let name = pairs.next().expect("fn_call always has 2 children").as_str().to_string();
(
Node::FnCall {
name,
expr: pairs.map(|p| parse_expr(p.into_inner()).map(|expr| expr.0)).collect::<Result<Vec<Node>, ParseError>>()?,
},
NodeMetadata::new(Unit::BASE_UNIT),
)
}
Rule::constant => {
let lit = parse_const(primary.into_inner().next().expect("constant should have atleast 1 child"));
(Node::Lit(lit), NodeMetadata::new(Unit::BASE_UNIT))
}
Rule::ident => {
let name = primary.as_str().to_string();
(Node::Var(name), NodeMetadata::new(Unit::BASE_UNIT))
}
Rule::expr => parse_expr(primary.into_inner())?,
Rule::float => {
let value = primary.as_str().parse::<f64>()?;
(Node::Lit(Literal::Float(value)), NodeMetadata::new(Unit::BASE_UNIT))
}
rule => unreachable!("unexpected rule: {:?}", rule),
})
})
.map_prefix(|op, rhs| {
let (rhs, rhs_metadata) = rhs?;
let op = match op.as_rule() {
Rule::neg => UnaryOp::Neg,
Rule::sqrt => UnaryOp::Sqrt,
rule => unreachable!("unexpected rule: {:?}", rule),
};
let node = Node::UnaryOp { expr: Box::new(rhs), op };
let unit = rhs_metadata.unit;
let unit = if !unit.is_base() {
match op {
UnaryOp::Sqrt if unit.length % 2 == 0 && unit.mass % 2 == 0 && unit.time % 2 == 0 => Unit {
length: unit.length / 2,
mass: unit.mass / 2,
time: unit.time / 2,
},
UnaryOp::Neg => unit,
op => return Err(ParseError::Type(TypeError::InvalidUnaryOp(unit, op))),
}
} else {
Unit::BASE_UNIT
};
Ok((node, NodeMetadata::new(unit)))
})
.map_postfix(|lhs, op| {
let (lhs_node, lhs_metadata) = lhs?;
let op = match op.as_rule() {
Rule::EOI => return Ok((lhs_node, lhs_metadata)),
Rule::fac => UnaryOp::Fac,
rule => unreachable!("unexpected rule: {:?}", rule),
};
if !lhs_metadata.unit.is_base() {
return Err(ParseError::Type(TypeError::InvalidUnaryOp(lhs_metadata.unit, op)));
}
Ok((Node::UnaryOp { expr: Box::new(lhs_node), op }, lhs_metadata))
})
.map_infix(|lhs, op, rhs| {
let (lhs, lhs_metadata) = lhs?;
let (rhs, rhs_metadata) = rhs?;
let op = match op.as_rule() {
Rule::add => BinaryOp::Add,
Rule::sub => BinaryOp::Sub,
Rule::mul => BinaryOp::Mul,
Rule::div => BinaryOp::Div,
Rule::pow => BinaryOp::Pow,
Rule::paren => BinaryOp::Mul,
rule => unreachable!("unexpected rule: {:?}", rule),
};
let (lhs_unit, rhs_unit) = (lhs_metadata.unit, rhs_metadata.unit);
let unit = match (!lhs_unit.is_base(), !rhs_unit.is_base()) {
(true, true) => match op {
BinaryOp::Mul => Unit {
length: lhs_unit.length + rhs_unit.length,
mass: lhs_unit.mass + rhs_unit.mass,
time: lhs_unit.time + rhs_unit.time,
},
BinaryOp::Div => Unit {
length: lhs_unit.length - rhs_unit.length,
mass: lhs_unit.mass - rhs_unit.mass,
time: lhs_unit.time - rhs_unit.time,
},
BinaryOp::Add | BinaryOp::Sub => {
if lhs_unit == rhs_unit {
lhs_unit
} else {
return Err(ParseError::Type(TypeError::InvalidBinaryOp(lhs_unit, op, rhs_unit)));
}
}
BinaryOp::Pow => {
return Err(ParseError::Type(TypeError::InvalidBinaryOp(lhs_unit, op, rhs_unit)));
}
},
(true, false) => match op {
BinaryOp::Add | BinaryOp::Sub => return Err(ParseError::Type(TypeError::InvalidBinaryOp(lhs_unit, op, Unit::BASE_UNIT))),
BinaryOp::Pow => {
//TODO: improve error type
//TODO: support 1 / int
if let Ok(Value::Number(Number::Real(val))) = rhs.eval(&EvalContext::default()) {
if (val - val as i32 as f64).abs() <= f64::EPSILON {
Unit {
length: lhs_unit.length * val as i32,
mass: lhs_unit.mass * val as i32,
time: lhs_unit.time * val as i32,
}
} else {
return Err(ParseError::Type(TypeError::InvalidBinaryOp(lhs_unit, op, Unit::BASE_UNIT)));
}
} else {
return Err(ParseError::Type(TypeError::InvalidBinaryOp(lhs_unit, op, Unit::BASE_UNIT)));
}
}
_ => lhs_unit,
},
(false, true) => match op {
BinaryOp::Add | BinaryOp::Sub | BinaryOp::Pow => return Err(ParseError::Type(TypeError::InvalidBinaryOp(Unit::BASE_UNIT, op, rhs_unit))),
_ => rhs_unit,
},
(false, false) => Unit::BASE_UNIT,
};
let node = Node::BinOp {
lhs: Box::new(lhs),
op,
rhs: Box::new(rhs),
};
Ok((node, NodeMetadata::new(unit)))
})
.parse(pairs)
}
//TODO: set up Unit test for Units
#[cfg(test)]
mod tests {
use super::*;
macro_rules! test_parser {
($($name:ident: $input:expr_2021 => $expected:expr_2021),* $(,)?) => {
$(
#[test]
fn $name() {
let result = Node::try_parse_from_str($input).unwrap();
assert_eq!(result.0, $expected);
}
)*
};
}
test_parser! {
test_parse_int_literal: "42" => Node::Lit(Literal::Float(42.)),
test_parse_float_literal: "3.14" => Node::Lit(Literal::Float(#[allow(clippy::approx_constant)] 3.14)),
test_parse_ident: "x" => Node::Var("x".to_string()),
test_parse_unary_neg: "-42" => Node::UnaryOp {
expr: Box::new(Node::Lit(Literal::Float(42.))),
op: UnaryOp::Neg,
},
test_parse_binary_add: "1 + 2" => Node::BinOp {
lhs: Box::new(Node::Lit(Literal::Float(1.))),
op: BinaryOp::Add,
rhs: Box::new(Node::Lit(Literal::Float(2.))),
},
test_parse_binary_mul: "3 * 4" => Node::BinOp {
lhs: Box::new(Node::Lit(Literal::Float(3.))),
op: BinaryOp::Mul,
rhs: Box::new(Node::Lit(Literal::Float(4.))),
},
test_parse_binary_pow: "2 ^ 3" => Node::BinOp {
lhs: Box::new(Node::Lit(Literal::Float(2.))),
op: BinaryOp::Pow,
rhs: Box::new(Node::Lit(Literal::Float(3.))),
},
test_parse_unary_sqrt: "sqrt(16)" => Node::UnaryOp {
expr: Box::new(Node::Lit(Literal::Float(16.))),
op: UnaryOp::Sqrt,
},
test_parse_sqr_ident: "sqr(16)" => Node::FnCall {
name:"sqr".to_string(),
expr: vec![Node::Lit(Literal::Float(16.))]
},
test_parse_complex_expr: "(1 + 2) 3 - 4 ^ 2" => Node::BinOp {
lhs: Box::new(Node::BinOp {
lhs: Box::new(Node::BinOp {
lhs: Box::new(Node::Lit(Literal::Float(1.))),
op: BinaryOp::Add,
rhs: Box::new(Node::Lit(Literal::Float(2.))),
}),
op: BinaryOp::Mul,
rhs: Box::new(Node::Lit(Literal::Float(3.))),
}),
op: BinaryOp::Sub,
rhs: Box::new(Node::BinOp {
lhs: Box::new(Node::Lit(Literal::Float(4.))),
op: BinaryOp::Pow,
rhs: Box::new(Node::Lit(Literal::Float(2.))),
}),
}
}
}
@@ -0,0 +1,126 @@
use crate::ast::{BinaryOp, UnaryOp};
use num_complex::ComplexFloat;
use std::f64::consts::PI;
pub type Complex = num_complex::Complex<f64>;
#[derive(Debug, PartialEq, Clone, Copy)]
pub enum Value {
Number(Number),
}
impl Value {
pub fn from_f64(x: f64) -> Self {
Self::Number(Number::Real(x))
}
pub fn as_real(&self) -> Option<f64> {
match self {
Self::Number(Number::Real(val)) => Some(*val),
_ => None,
}
}
}
impl From<f64> for Value {
fn from(x: f64) -> Self {
Self::from_f64(x)
}
}
impl core::fmt::Display for Value {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Value::Number(num) => num.fmt(f),
}
}
}
#[derive(Debug, PartialEq, Clone, Copy)]
pub enum Number {
Real(f64),
Complex(Complex),
}
impl std::fmt::Display for Number {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Number::Real(real) => real.fmt(f),
Number::Complex(complex) => complex.fmt(f),
}
}
}
impl Number {
pub fn binary_op(self, op: BinaryOp, other: Number) -> Number {
match (self, other) {
(Number::Real(lhs), Number::Real(rhs)) => {
let result = match op {
BinaryOp::Add => lhs + rhs,
BinaryOp::Sub => lhs - rhs,
BinaryOp::Mul => lhs * rhs,
BinaryOp::Div => lhs / rhs,
BinaryOp::Pow => lhs.powf(rhs),
};
Number::Real(result)
}
(Number::Complex(lhs), Number::Complex(rhs)) => {
let result = match op {
BinaryOp::Add => lhs + rhs,
BinaryOp::Sub => lhs - rhs,
BinaryOp::Mul => lhs * rhs,
BinaryOp::Div => lhs / rhs,
BinaryOp::Pow => lhs.powc(rhs),
};
Number::Complex(result)
}
(Number::Real(lhs), Number::Complex(rhs)) => {
let lhs_complex = Complex::new(lhs, 0.);
let result = match op {
BinaryOp::Add => lhs_complex + rhs,
BinaryOp::Sub => lhs_complex - rhs,
BinaryOp::Mul => lhs_complex * rhs,
BinaryOp::Div => lhs_complex / rhs,
BinaryOp::Pow => lhs_complex.powc(rhs),
};
Number::Complex(result)
}
(Number::Complex(lhs), Number::Real(rhs)) => {
let rhs_complex = Complex::new(rhs, 0.);
let result = match op {
BinaryOp::Add => lhs + rhs_complex,
BinaryOp::Sub => lhs - rhs_complex,
BinaryOp::Mul => lhs * rhs_complex,
BinaryOp::Div => lhs / rhs_complex,
BinaryOp::Pow => lhs.powf(rhs),
};
Number::Complex(result)
}
}
}
pub fn unary_op(self, op: UnaryOp) -> Number {
match self {
Number::Real(real) => match op {
UnaryOp::Neg => Number::Real(-real),
UnaryOp::Sqrt => Number::Real(real.sqrt()),
UnaryOp::Fac => todo!("Implement factorial"),
},
Number::Complex(complex) => match op {
UnaryOp::Neg => Number::Complex(-complex),
UnaryOp::Sqrt => Number::Complex(complex.sqrt()),
UnaryOp::Fac => todo!("Implement factorial"),
},
}
}
pub fn from_f64(x: f64) -> Self {
Self::Real(x)
}
}
@@ -0,0 +1,2 @@
tests/images/*
!tests/images/.gitkeep
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,39 @@
[workspace]
members = ["rawkit-proc-macros"]
resolver = "2"
[workspace.dependencies]
quote = "1.0"
syn = "2.0"
[package]
name = "rawkit"
version = "0.1.0"
edition = "2024"
authors = ["Graphite Authors <contact@graphite.art>"]
description = "A library to extract images from camera raw files"
license = "MIT OR Apache-2.0"
readme = "README.md"
keywords = ["raw", "tiff", "camera", "image"]
categories = ["multimedia::images", "multimedia::encoding"]
homepage = "https://github.com/GraphiteEditor/Graphite/tree/master/libraries/rawkit"
repository = "https://github.com/GraphiteEditor/Graphite/tree/master/libraries/rawkit"
documentation = "https://docs.rs/rawkit"
[features]
rawkit-tests = ["dep:image", "dep:libraw-rs", "dep:reqwest", "dep:rayon"]
[dependencies]
# Local dependencies
rawkit-proc-macros = { version = "0.1.0", path = "rawkit-proc-macros" }
# Required dependencies
bitstream-io = "4.9.0"
num_enum = "0.7.5"
thiserror = "2.0.17"
# Optional dependencies (should be dev dependencies, but Cargo currently doesn't allow optional dev dependencies)
image = { version = "0.25.9", optional = true }
reqwest = { version = "0.13", optional = true, features = ["blocking"] }
libraw-rs = { version = "0.0.4", optional = true }
rayon = { version = "1.11.0", optional = true }
@@ -0,0 +1,201 @@
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.
@@ -0,0 +1,17 @@
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.
@@ -0,0 +1,40 @@
[crates.io](https://crates.io/crates/rawkit) • [docs.rs](https://docs.rs/rawkit) • [repo](https://github.com/GraphiteEditor/Graphite/tree/master/libraries/rawkit)
# Rawkit 🚀
A library to extract images from camera raw files.
It currently only works with the `.arw` files from Sony's cameras. In the future, the library will add support for all other major camera manufacturers.
Rawkit is built for the needs of [Graphite](https://graphite.art), an open source 2D graphics editor. We hope it may be useful to others, but presently Graphite is its primary user. Pull requests are welcomed for new cameras, features, code cleanup, ergonomic enhancements, performance improvements, and documentation clarifications.
### Using Rawkit
```rust
use rawkit::RawImage;
use rawkit::tiff::values::Transform;
// Open a file for reading
let file = BufReader::new(File::open("example.arw")?);
// Decode the file to extract the raw pixels and its associated metadata
let mut raw_image = RawImage::decode(file);
// All the raw pixel data and metadata is stored within `raw_image`
println!("Initial Bayer pixel values: {:?}", raw_image.data[:10]);
println!("Image size: {} x {}", raw_image.width, raw_image.height);
println!("CFA Pattern: {:?}", raw_image.cfa_pattern);
println!("Camera Model: {:?}", raw_image.camera_model);
println!("White balance: {:?}", raw_image.white_balance);
// The metadata could also be edited if the extracted metadata needs to be customized
raw_image.white_balance = Some([2609, 1024, 1024, 1220]); // For RGGB camera
raw_image.transform = Transform::Rotate90;
// Process the raw image into an RGB image
let image = raw_image.process_8bit();
// The final image data will be stored within `image`
println!("Initial RGB pixel values: {:?}", image.data[:10]);
println!("Image size: {} x {}", image.width, image.height);
```
@@ -0,0 +1 @@
xyz_to_camera = [0.9437, -0.2812, -0.0774, -0.8405, 1.6215, 0.2291, -0.0709, 0.0596, 0.7181]
@@ -0,0 +1 @@
xyz_to_camera = [0.9847, -0.3091, -0.0929, -0.8485, 1.6346, 0.2225, -0.0714, 0.0595, 0.7103]
@@ -0,0 +1 @@
xyz_to_camera = [0.9847, -0.3091, -0.0929, -0.8485, 1.6346, 0.2225, -0.0714, 0.0595, 0.7103]
@@ -0,0 +1 @@
xyz_to_camera = [0.9847, -0.3091, -0.0929, -0.8485, 1.6346, 0.2225, -0.0714, 0.0595, 0.7103]
@@ -0,0 +1 @@
xyz_to_camera = [0.6038, -0.1484, -0.0579, -0.9145, 1.6746, 0.2512, -0.0875, 0.0746, 0.7218]
@@ -0,0 +1 @@
xyz_to_camera = [0.4950, -0.0580, -0.0103, -0.5228, 1.2542, 0.3029, -0.0709, 0.1435, 0.7371]
@@ -0,0 +1 @@
xyz_to_camera = [0.5775, -0.0805, -0.0359, -0.8573, 1.6294, 0.2391, -0.1943, 0.2342, 0.7249]
@@ -0,0 +1 @@
xyz_to_camera = [0.5413, -0.1162, -0.0365, -0.5665, 1.3098, 0.2866, -0.0608, 0.1179, 0.8440]
@@ -0,0 +1 @@
xyz_to_camera = [0.5209, -0.1072, -0.0397, -0.8845, 1.6121, 0.2919, -0.1618, 0.1802, 0.8654]
@@ -0,0 +1 @@
xyz_to_camera = [0.6435, -0.1903, -0.0536, -0.4722, 1.2449, 0.2550, -0.0663, 0.1363, 0.6517]
@@ -0,0 +1 @@
xyz_to_camera = [0.6660, -0.1918, -0.0471, -0.4613, 1.2398, 0.2485, -0.0649, 0.1433, 0.6447]
@@ -0,0 +1 @@
xyz_to_camera = [0.8161, -0.2947, -0.0739, -0.4811, 1.2668, 0.2389, -0.0437, 0.1229, 0.6524]
@@ -0,0 +1 @@
xyz_to_camera = [0.5991, -0.1456, -0.0455, -0.4764, 1.2135, 0.2980, -0.0707, 0.1425, 0.6701]
@@ -0,0 +1 @@
xyz_to_camera = [0.5991, -0.1456, -0.0455, -0.4764, 1.2135, 0.2980, -0.0707, 0.1425, 0.6701]
@@ -0,0 +1 @@
xyz_to_camera = [0.7657, -0.2847, -0.0607, -0.4083, 1.1966, 0.2389, -0.0684, 0.1418, 0.5844]
@@ -0,0 +1 @@
xyz_to_camera = [0.5973, -0.1695, -0.0419, -0.3826, 1.1797, 0.2293, -0.0639, 0.1398, 0.5789]
@@ -0,0 +1 @@
xyz_to_camera = [0.7657, -0.2847, -0.0607, -0.4083, 1.1966, 0.2389, -0.0684, 0.1418, 0.5844]
@@ -0,0 +1 @@
xyz_to_camera = [0.5973, -0.1695, -0.0419, -0.3826, 1.1797, 0.2293, -0.0639, 0.1398, 0.5789]
@@ -0,0 +1 @@
xyz_to_camera = [0.7657, -0.2847, -0.0607, -0.4083, 1.1966, 0.2389, -0.0684, 0.1418, 0.5844]
@@ -0,0 +1 @@
xyz_to_camera = [0.6972, -0.2408, -0.0600, -0.4330, 1.2101, 0.2515, -0.0388, 0.1277, 0.5847]
@@ -0,0 +1 @@
xyz_to_camera = [0.7460, -0.2365, -0.0588, -0.5687, 1.3442, 0.2474, -0.0624, 0.1156, 0.6584]
@@ -0,0 +1 @@
xyz_to_camera = [0.8200, -0.2976, -0.0719, -0.4296, 1.2053, 0.2532, -0.0429, 0.1282, 0.5774]
@@ -0,0 +1 @@
xyz_to_camera = [0.5271, -0.0712, -0.0347, -0.6153, 1.3653, 0.2763, -0.1601, 0.2366, 0.7242]
@@ -0,0 +1 @@
xyz_to_camera = [0.7374, -0.2389, -0.0551, -0.5435, 1.3162, 0.2519, -0.1006, 0.1795, 0.6552]
@@ -0,0 +1 @@
xyz_to_camera = [0.6640, -0.1847, -0.0503, -0.5238, 1.3010, 0.2474, -0.0993, 0.1673, 0.6527]
@@ -0,0 +1 @@
xyz_to_camera = [0.7662, -0.2686, -0.0660, -0.5240, 1.2965, 0.2530, -0.0796, 0.1508, 0.6167]
@@ -0,0 +1 @@
xyz_to_camera = [0.8200, -0.2976, -0.0719, -0.4296, 1.2053, 0.2532, -0.0429, 0.1282, 0.5774]
@@ -0,0 +1 @@
xyz_to_camera = [0.5838, -0.1430, -0.0246, -0.3497, 1.1477, 0.2297, -0.0748, 0.1885, 0.5778]
@@ -0,0 +1 @@
xyz_to_camera = [0.6389, -0.1703, -0.0378, -0.4562, 1.2265, 0.2587, -0.0670, 0.1489, 0.6550]
@@ -0,0 +1 @@
xyz_to_camera = [0.6389, -0.1703, -0.0378, -0.4562, 1.2265, 0.2587, -0.0670, 0.1489, 0.6550]
@@ -0,0 +1 @@
xyz_to_camera = [0.9811, -0.3908, -0.0752, -0.3704, 1.1577, 0.2417, -0.0073, 0.0950, 0.5980]
@@ -0,0 +1 @@
xyz_to_camera = [0.6549, -0.1550, -0.0436, -0.4880, 1.2435, 0.2753, -0.0854, 0.1868, 0.6976]
@@ -0,0 +1 @@
xyz_to_camera = [0.6129, -0.1545, -0.0418, -0.4930, 1.2490, 0.2743, -0.0977, 0.1693, 0.6615]
@@ -0,0 +1 @@
xyz_to_camera = [0.6129, -0.1545, -0.0418, -0.4930, 1.2490, 0.2743, -0.0977, 0.1693, 0.6615]
@@ -0,0 +1 @@
xyz_to_camera = [0.6129, -0.1545, -0.0418, -0.4930, 1.2490, 0.2743, -0.0977, 0.1693, 0.6615]
@@ -0,0 +1 @@
xyz_to_camera = [0.5491, -0.1192, -0.0363, -0.4951, 1.2342, 0.2948, -0.0911, 0.1722, 0.7192]
@@ -0,0 +1 @@
xyz_to_camera = [0.8280, -0.2987, -0.0703, -0.3531, 1.1645, 0.2133, -0.0550, 0.1542, 0.5312]
@@ -0,0 +1 @@
xyz_to_camera = [0.8280, -0.2987, -0.0703, -0.3531, 1.1645, 0.2133, -0.0550, 0.1542, 0.5312]
@@ -0,0 +1 @@
xyz_to_camera = [0.6912, -0.2127, -0.0469, -0.4470, 1.2175, 0.2587, -0.0398, 0.1478, 0.6492]
@@ -0,0 +1 @@
xyz_to_camera = [0.6355, -0.2067, -0.0490, -0.3653, 1.1542, 0.2400, -0.0406, 0.1258, 0.5506]
@@ -0,0 +1,23 @@
[package]
name = "rawkit-proc-macros"
version = "0.1.0"
edition = "2024"
authors = ["Graphite Authors <contact@graphite.art>"]
description = "Procedural macros for Rawkit"
license = "MIT OR Apache-2.0"
readme = "README.md"
homepage = "https://github.com/GraphiteEditor/Graphite/tree/master/libraries/rawkit/rawkit-proc-macros"
repository = "https://github.com/GraphiteEditor/Graphite/tree/master/libraries/rawkit/rawkit-proc-macros"
documentation = "https://docs.rs/rawkit-proc-macros"
[lib]
proc-macro = true
[dependencies]
# Workspace dependencies
quote = { workspace = true }
syn = { workspace = true }
# Required dependencies
toml = "0.9.10+spec-1.1.0"
proc-macro2 = "1.0.103"
@@ -0,0 +1,201 @@
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.
@@ -0,0 +1,17 @@
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.
@@ -0,0 +1,13 @@
# Rawkit-proc-macros
Procedural macros for Rawkit.
This library is intended to be used by Rawkit. You should not be depending on this crate directly.
### Tag
A derive macro that helps to specify which metadata needs to be extracted from IFD.
### build_camera_data
A procedural macro that reads the data of all cameras from the toml files and returns the bundled data. Helps to include camera data as part of binary.
@@ -0,0 +1,97 @@
use proc_macro::TokenStream;
use quote::{ToTokens, quote};
use std::fs;
use std::path::Path;
use toml::{Table, Value};
enum CustomValue {
String(String),
Integer(i64),
Float(f64),
Boolean(bool),
Array(Vec<CustomValue>),
}
impl ToTokens for CustomValue {
fn to_tokens(&self, tokens: &mut proc_macro2::TokenStream) {
match self {
CustomValue::String(x) => x.to_tokens(tokens),
CustomValue::Integer(x) => {
let x: proc_macro2::TokenStream = format!("{:?}", x).parse().unwrap();
x.to_tokens(tokens)
}
CustomValue::Float(x) => {
let x: proc_macro2::TokenStream = format!("{:?}", x).parse().unwrap();
x.to_tokens(tokens)
}
CustomValue::Boolean(x) => x.to_tokens(tokens),
CustomValue::Array(x) => quote! { [ #( #x ),* ] }.to_tokens(tokens),
}
}
}
impl From<Value> for CustomValue {
fn from(value: Value) -> Self {
match value {
Value::String(x) => CustomValue::String(x),
Value::Integer(x) => CustomValue::Integer(x),
Value::Float(x) => CustomValue::Float(x),
Value::Boolean(x) => CustomValue::Boolean(x),
Value::Array(x) => CustomValue::Array(x.into_iter().map(|x| x.into()).collect()),
_ => panic!("Unsupported data type"),
}
}
}
pub fn build_camera_data() -> TokenStream {
let mut camera_data: Vec<(String, Table)> = Vec::new();
let mut path = Path::new(&std::env::var("CARGO_MANIFEST_DIR").unwrap()).to_path_buf();
path.push("camera_data");
fs::read_dir(path).unwrap().for_each(|entry| {
let company_name_path = entry.unwrap().path();
if !company_name_path.is_dir() {
panic!("camera_data should only contain folders of company names")
}
let company_name = company_name_path.file_name().unwrap().to_str().unwrap().to_string();
fs::read_dir(company_name_path).unwrap().for_each(|entry| {
let model_path = entry.unwrap().path();
if !model_path.is_file() || model_path.extension().unwrap() != "toml" {
panic!("The folders within camera_data should only contain toml files")
}
let name = company_name.clone() + " " + model_path.file_stem().unwrap().to_str().unwrap();
let mut values: Table = toml::from_str(&fs::read_to_string(model_path).unwrap()).unwrap();
if let Some(val) = values.get_mut("xyz_to_camera") {
*val = Value::Array(val.as_array().unwrap().iter().map(|x| Value::Integer((x.as_float().unwrap() * 10_000.) as i64)).collect());
}
camera_data.push((name, values))
});
});
let x: Vec<_> = camera_data
.iter()
.map(|(name, camera_data)| {
let keys: Vec<_> = camera_data.keys().map(|key| syn::Ident::new(key, proc_macro2::Span::call_site())).collect();
let values: Vec<CustomValue> = camera_data.values().cloned().map(|x| x.into()).collect();
quote! {
(
#name,
CameraData {
#( #keys: #values, )*
..CameraData::DEFAULT
}
)
}
})
.collect();
quote!([ #(#x),* ]).into()
}
@@ -0,0 +1,16 @@
extern crate proc_macro;
mod build_camera_data;
mod tag_derive;
use proc_macro::TokenStream;
#[proc_macro_derive(Tag)]
pub fn tag_derive(input: TokenStream) -> TokenStream {
tag_derive::tag_derive(input)
}
#[proc_macro]
pub fn build_camera_data(_: TokenStream) -> TokenStream {
build_camera_data::build_camera_data()
}
@@ -0,0 +1,43 @@
use proc_macro::TokenStream;
use quote::{format_ident, quote};
use syn::{Data, DeriveInput, Fields};
pub fn tag_derive(input: TokenStream) -> TokenStream {
let ast: DeriveInput = syn::parse(input).unwrap();
let name = &ast.ident;
let data_struct = if let Data::Struct(data_struct) = ast.data {
data_struct
} else {
panic!("Tag trait can only be derived for structs")
};
let named_fields = if let Fields::Named(named_fields) = data_struct.fields {
named_fields
} else {
panic!("Tag trait can only be derived for structs with named_fields")
};
let struct_idents: Vec<_> = named_fields.named.iter().map(|field| field.ident.clone().unwrap()).collect();
let struct_types: Vec<_> = named_fields.named.iter().map(|field| field.ty.clone()).collect();
let new_name = format_ident!("_{}", name);
let r#gen = quote! {
struct #new_name {
#( #struct_idents: <#struct_types as Tag>::Output ),*
}
impl Tag for #name {
type Output = #new_name;
fn get<R: Read + Seek>(ifd: &Ifd, file: &mut TiffRead<R>) -> Result<Self::Output, TiffError> {
#( let #struct_idents = <#struct_types as Tag>::get(ifd, file)?; )*
Ok(#new_name { #( #struct_idents ),* })
}
}
};
r#gen.into()
}
@@ -0,0 +1,104 @@
use crate::tiff::Ifd;
use crate::tiff::file::TiffRead;
use crate::tiff::tags::SonyDataOffset;
use crate::{OrientationValue, RawImage, SubtractBlack};
use bitstream_io::{BE, BitRead, BitReader, Endianness};
use std::io::{Read, Seek};
pub fn decode_a100<R: Read + Seek>(ifd: Ifd, file: &mut TiffRead<R>) -> RawImage {
let data_offset = ifd.get_value::<SonyDataOffset, _>(file).unwrap();
let image_width = 3881;
let image_height = 2608;
file.seek_from_start(data_offset).unwrap();
let mut image = sony_arw_load_raw(image_width, image_height, &mut BitReader::<_, BE>::new(file)).unwrap();
let len = image.len();
image[len - image_width..].fill(0);
RawImage {
data: image,
width: image_width,
height: image_height,
cfa_pattern: todo!(),
#[allow(unreachable_code)]
maximum: (1 << 12) - 1,
black: SubtractBlack::None,
orientation: OrientationValue::Horizontal,
camera_model: None,
camera_white_balance: None,
white_balance: None,
camera_to_rgb: None,
}
}
fn read_and_huffman_decode_file<R: Read + Seek, E: Endianness>(huff: &[u16], file: &mut BitReader<R, E>) -> u32 {
let number_of_bits = huff[0].into();
let huffman_table = &huff[1..];
// `number_of_bits` will be no more than 32, so the result is put into a u32
let bits: u32 = file.read_var(number_of_bits).unwrap();
let bits = bits as usize;
let bits_to_seek_from = huffman_table[bits].to_le_bytes()[1] as i64 - number_of_bits as i64;
file.seek_bits(std::io::SeekFrom::Current(bits_to_seek_from)).unwrap();
huffman_table[bits].to_le_bytes()[0].into()
}
fn read_n_bits_from_file<R: Read + Seek, E: Endianness>(number_of_bits: u32, file: &mut BitReader<R, E>) -> u32 {
// `number_of_bits` will be no more than 32, so the result is put into a u32
file.read_var(number_of_bits).unwrap()
}
/// ljpeg is a lossless variant of JPEG which gets used for decoding the embedded (thumbnail) preview images in raw files
fn ljpeg_diff<R: Read + Seek, E: Endianness>(huff: &[u16], file: &mut BitReader<R, E>, dng_version: Option<u32>) -> i32 {
let length = read_and_huffman_decode_file(huff, file);
if length == 16 && dng_version.map(|x| x >= 0x1010000).unwrap_or(true) {
return -32768;
}
let diff = read_n_bits_from_file(length, file) as i32;
if length == 0 || (diff & (1 << (length - 1))) == 0 { diff - (1 << length) - 1 } else { diff }
}
fn sony_arw_load_raw<R: Read + Seek>(width: usize, height: usize, file: &mut BitReader<R, BE>) -> Option<Vec<u16>> {
const TABLE: [u16; 18] = [
0x0f11, 0x0f10, 0x0e0f, 0x0d0e, 0x0c0d, 0x0b0c, 0x0a0b, 0x090a, 0x0809, 0x0708, 0x0607, 0x0506, 0x0405, 0x0304, 0x0303, 0x0300, 0x0202, 0x0201,
];
let mut huffman_table = [0_u16; 32770];
// The first element is the number of bits to read
huffman_table[0] = 15;
let mut n = 0;
for x in TABLE {
let first_byte = x >> 8;
let repeats = 0x8000 >> first_byte;
for _ in 0_u16..repeats {
n += 1;
huffman_table[n] = x;
}
}
let mut sum = 0;
let mut image = vec![0_u16; width * height];
for column in (0..width).rev() {
for row in (0..height).step_by(2).chain((1..height).step_by(2)) {
sum += ljpeg_diff(&huffman_table, file, None);
if (sum >> 12) != 0 {
return None;
}
if row < height {
image[row * width + column] = sum as u16;
}
}
}
Some(image)
}
@@ -0,0 +1,126 @@
use crate::tiff::file::{Endian, TiffRead};
use crate::tiff::tags::{BitsPerSample, CfaPattern, CfaPatternDim, Compression, ImageLength, ImageWidth, SonyToneCurve, StripByteCounts, StripOffsets, Tag, WhiteBalanceRggbLevels};
use crate::tiff::values::{CompressionValue, CurveLookupTable};
use crate::tiff::{Ifd, TiffError};
use crate::{OrientationValue, RawImage, SubtractBlack};
use rawkit_proc_macros::Tag;
use std::io::{Read, Seek};
#[allow(dead_code)]
#[derive(Tag)]
struct Arw2Ifd {
image_width: ImageWidth,
image_height: ImageLength,
bits_per_sample: BitsPerSample,
compression: Compression,
cfa_pattern: CfaPattern,
cfa_pattern_dim: CfaPatternDim,
strip_offsets: StripOffsets,
strip_byte_counts: StripByteCounts,
sony_tone_curve: SonyToneCurve,
white_balance_levels: Option<WhiteBalanceRggbLevels>,
}
pub fn decode<R: Read + Seek>(ifd: Ifd, file: &mut TiffRead<R>) -> RawImage {
let ifd = ifd.get_value::<Arw2Ifd, _>(file).unwrap();
assert!(ifd.strip_offsets.len() == ifd.strip_byte_counts.len());
assert!(ifd.strip_offsets.len() == 1);
assert!(ifd.compression == CompressionValue::Sony_ARW_Compressed);
let image_width: usize = ifd.image_width.try_into().unwrap();
let image_height: usize = ifd.image_height.try_into().unwrap();
let bits_per_sample: usize = ifd.bits_per_sample.into();
assert!(bits_per_sample == 12);
let [cfa_pattern_width, cfa_pattern_height] = ifd.cfa_pattern_dim;
assert!(cfa_pattern_width == 2 && cfa_pattern_height == 2);
file.seek_from_start(ifd.strip_offsets[0]).unwrap();
let mut image = sony_arw2_load_raw(image_width, image_height, ifd.sony_tone_curve, file).unwrap();
// Converting the bps from 12 to 14 so that ARW 2.3.1 and 2.3.5 have the same 14 bps.
image.iter_mut().for_each(|x| *x <<= 2);
RawImage {
data: image,
width: image_width,
height: image_height,
cfa_pattern: ifd.cfa_pattern.try_into().unwrap(),
maximum: (1 << 14) - 1,
black: SubtractBlack::CfaGrid([512, 512, 512, 512]), // TODO: Find the correct way to do this
orientation: OrientationValue::Horizontal,
camera_model: None,
camera_white_balance: ifd.white_balance_levels.map(|arr| arr.map(|x| x as f64)),
white_balance: None,
camera_to_rgb: None,
}
}
fn as_u32(buffer: &[u8], endian: Endian) -> Option<u32> {
Some(match endian {
Endian::Little => u32::from_le_bytes(buffer.try_into().ok()?),
Endian::Big => u32::from_be_bytes(buffer.try_into().ok()?),
})
}
fn as_u16(buffer: &[u8], endian: Endian) -> Option<u16> {
Some(match endian {
Endian::Little => u16::from_le_bytes(buffer.try_into().ok()?),
Endian::Big => u16::from_be_bytes(buffer.try_into().ok()?),
})
}
fn sony_arw2_load_raw<R: Read + Seek>(width: usize, height: usize, curve: CurveLookupTable, file: &mut TiffRead<R>) -> Option<Vec<u16>> {
let mut image = vec![0_u16; height * width];
let mut data = vec![0_u8; width + 1];
for row in 0..height {
file.read_exact(&mut data[0..width]).unwrap();
let mut column = 0;
let mut data_index = 0;
while column < width - 30 {
let data_value = as_u32(&data[data_index..][..4], file.endian()).unwrap();
let max = (0x7ff & data_value) as u16;
let min = (0x7ff & data_value >> 11) as u16;
let index_to_set_max = 0x0f & data_value >> 22;
let index_to_set_min = 0x0f & data_value >> 26;
let max_minus_min = max as i32 - min as i32;
let shift_by_bits = (0..4).find(|&shift| (0x80 << shift) > max_minus_min).unwrap_or(4);
let mut pixels = [0_u16; 16];
let mut bit = 30;
for (i, pixel) in pixels.iter_mut().enumerate() {
*pixel = match () {
_ if i as u32 == index_to_set_max => max,
_ if i as u32 == index_to_set_min => min,
_ => {
let result = as_u16(&data[(data_index + (bit >> 3))..][..2], file.endian()).unwrap();
let result = ((result >> (bit & 7)) & 0x07f) << shift_by_bits;
bit += 7;
(result + min).min(0x7ff)
}
};
}
for value in pixels {
image[row * width + column] = curve.get((value << 1).into()) >> 2;
// Skip between interlaced columns
column += 2;
}
// Switch to the opposite interlaced columns
column -= if column & 1 == 0 { 31 } else { 1 };
data_index += 16;
}
}
Some(image)
}
@@ -0,0 +1,3 @@
pub mod arw1;
pub mod arw2;
pub mod uncompressed;
@@ -0,0 +1,67 @@
use crate::tiff::file::TiffRead;
use crate::tiff::tags::{BitsPerSample, BlackLevel, CfaPattern, CfaPatternDim, Compression, ImageLength, ImageWidth, RowsPerStrip, StripByteCounts, StripOffsets, Tag, WhiteBalanceRggbLevels};
use crate::tiff::values::CompressionValue;
use crate::tiff::{Ifd, TiffError};
use crate::{OrientationValue, RawImage, SubtractBlack};
use rawkit_proc_macros::Tag;
use std::io::{Read, Seek};
#[allow(dead_code)]
#[derive(Tag)]
struct ArwUncompressedIfd {
image_width: ImageWidth,
image_height: ImageLength,
rows_per_strip: RowsPerStrip,
bits_per_sample: BitsPerSample,
compression: Compression,
black_level: BlackLevel,
cfa_pattern: CfaPattern,
cfa_pattern_dim: CfaPatternDim,
strip_offsets: StripOffsets,
strip_byte_counts: StripByteCounts,
white_balance_levels: Option<WhiteBalanceRggbLevels>,
}
pub fn decode<R: Read + Seek>(ifd: Ifd, file: &mut TiffRead<R>) -> RawImage {
let ifd = ifd.get_value::<ArwUncompressedIfd, _>(file).unwrap();
assert!(ifd.strip_offsets.len() == ifd.strip_byte_counts.len());
assert!(ifd.strip_offsets.len() == 1);
assert!(ifd.compression == CompressionValue::Uncompressed);
let image_width: usize = ifd.image_width.try_into().unwrap();
let image_height: usize = ifd.image_height.try_into().unwrap();
let rows_per_strip: usize = ifd.rows_per_strip.try_into().unwrap();
let bits_per_sample: usize = ifd.bits_per_sample.into();
let [cfa_pattern_width, cfa_pattern_height] = ifd.cfa_pattern_dim;
assert!(cfa_pattern_width == 2 && cfa_pattern_height == 2);
let mut image: Vec<u16> = Vec::with_capacity(image_height * image_width);
for i in 0..ifd.strip_offsets.len() {
file.seek_from_start(ifd.strip_offsets[i]).unwrap();
let last = i == ifd.strip_offsets.len();
let rows = if last { image_height % rows_per_strip } else { rows_per_strip };
for _ in 0..rows {
for _ in 0..image_width {
image.push(file.read_u16().unwrap());
}
}
}
RawImage {
data: image,
width: image_width,
height: image_height,
cfa_pattern: ifd.cfa_pattern.try_into().unwrap(),
maximum: if bits_per_sample == 16 { u16::MAX } else { (1 << bits_per_sample) - 1 },
black: SubtractBlack::CfaGrid(ifd.black_level),
orientation: OrientationValue::Horizontal,
camera_model: None,
camera_white_balance: ifd.white_balance_levels.map(|arr| arr.map(|x| x as f64)),
white_balance: None,
camera_to_rgb: None,
}
}
@@ -0,0 +1,81 @@
use crate::{Pixel, RawImage};
fn average(data: &[u16], indexes: impl Iterator<Item = i64>) -> u16 {
let mut sum = 0;
let mut count = 0;
for index in indexes {
if index >= 0 && (index as usize) < data.len() {
sum += data[index as usize] as u32;
count += 1;
}
}
(sum / count) as u16
}
impl RawImage {
pub fn linear_demosaic_iter(&self) -> impl Iterator<Item = Pixel> + use<'_> {
match self.cfa_pattern {
[0, 1, 1, 2] => self.linear_demosaic_rggb_iter(),
_ => todo!(),
}
}
fn linear_demosaic_rggb_iter(&self) -> impl Iterator<Item = Pixel> + use<'_> {
let width = self.width as i64;
let height = self.height as i64;
(0..height).flat_map(move |row| {
let row_by_width = row * width;
(0..width).map(move |column| {
let pixel_index = row_by_width + column;
let vertical_indexes = [pixel_index + width, pixel_index - width];
let horizontal_indexes = [pixel_index + 1, pixel_index - 1];
let cross_indexes = [pixel_index + width, pixel_index - width, pixel_index + 1, pixel_index - 1];
let diagonal_indexes = [pixel_index + width + 1, pixel_index - width + 1, pixel_index + width - 1, pixel_index - width - 1];
let pixel_index = pixel_index as usize;
match (row % 2 == 0, column % 2 == 0) {
(true, true) => Pixel {
values: [
self.data[pixel_index],
average(&self.data, cross_indexes.into_iter()),
average(&self.data, diagonal_indexes.into_iter()),
],
row: row as usize,
column: column as usize,
},
(true, false) => Pixel {
values: [
average(&self.data, horizontal_indexes.into_iter()),
self.data[pixel_index],
average(&self.data, vertical_indexes.into_iter()),
],
row: row as usize,
column: column as usize,
},
(false, true) => Pixel {
values: [
average(&self.data, vertical_indexes.into_iter()),
self.data[pixel_index],
average(&self.data, horizontal_indexes.into_iter()),
],
row: row as usize,
column: column as usize,
},
(false, false) => Pixel {
values: [
average(&self.data, diagonal_indexes.into_iter()),
average(&self.data, cross_indexes.into_iter()),
self.data[pixel_index],
],
row: row as usize,
column: column as usize,
},
}
})
})
}
}
@@ -0,0 +1 @@
pub mod linear_demosaicing;
@@ -0,0 +1,300 @@
pub mod decoder;
pub mod demosaicing;
pub mod metadata;
pub mod postprocessing;
pub mod preprocessing;
pub mod processing;
pub mod tiff;
use crate::metadata::identify::CameraModel;
use processing::{Pixel, PixelTransform, RawPixel, RawPixelTransform};
use rawkit_proc_macros::Tag;
use std::io::{Read, Seek};
use thiserror::Error;
use tiff::file::TiffRead;
use tiff::tags::{Compression, ImageLength, ImageWidth, Orientation, StripByteCounts, SubIfd, Tag, ThumbnailLength, ThumbnailOffset};
use tiff::values::{CompressionValue, OrientationValue};
use tiff::{Ifd, TiffError};
pub(crate) const CHANNELS_IN_RGB: usize = 3;
pub(crate) type Histogram = [[usize; 0x2000]; CHANNELS_IN_RGB];
pub enum ThumbnailFormat {
Jpeg,
Unsupported,
}
/// A thumbnail image extracted from the raw file. This is usually a JPEG image.
pub struct ThumbnailImage {
pub data: Vec<u8>,
pub format: ThumbnailFormat,
}
/// The amount of black level to be subtracted from Raw Image.
pub enum SubtractBlack {
/// Don't subtract any value.
None,
/// Subtract a singular value for all pixels in Bayer CFA Grid.
Value(u16),
/// Subtract the appropriate value for pixels in Bayer CFA Grid.
CfaGrid([u16; 4]),
}
/// Represents a Raw Image along with its metadata.
pub struct RawImage {
/// Raw pixel data stored in linear fashion.
pub data: Vec<u16>,
/// Width of the raw image.
pub width: usize,
/// Height of the raw image.
pub height: usize,
/// Bayer CFA pattern used to arrange pixels in [`RawImage::data`].
///
/// It encodes Red, Blue and Green as 0, 1, and 2 respectively.
pub cfa_pattern: [u8; 4],
/// Transformation to be applied to negate the orientation of camera.
pub orientation: OrientationValue,
/// The maximum possible value of pixel that the camera sensor could give.
pub maximum: u16,
/// The minimum possible value of pixel that the camera sensor could give.
///
/// Used to subtract the black level from the raw image.
pub black: SubtractBlack,
/// Information regarding the company and model of the camera.
pub camera_model: Option<CameraModel>,
/// White balance specified in the metadata of the raw file.
///
/// It represents the 4 values of CFA Grid which follows the same pattern as [`RawImage::cfa_pattern`].
pub camera_white_balance: Option<[f64; 4]>,
/// White balance of the raw image.
///
/// It is the same as [`RawImage::camera_white_balance`] if the raw file contains the metadata.
/// Otherwise it falls back to calculating the white balance from the color space conversion matrix.
///
/// It represents the 4 values of CFA Grid which follows the same pattern as [`RawImage::cfa_pattern`].
pub white_balance: Option<[f64; 4]>,
/// Color space conversion matrix to convert from camera's color space to sRGB.
pub camera_to_rgb: Option<[[f64; 3]; 3]>,
}
/// Represents the final RGB Image.
pub struct Image<T> {
/// Pixel data stored in a linear fashion.
pub data: Vec<T>,
/// Width of the image.
pub width: usize,
/// Height of the image.
pub height: usize,
/// The number of color channels in the image.
///
/// We can assume this will be 3 for all non-obscure, modern cameras.
/// See <https://github.com/GraphiteEditor/Graphite/pull/1923#discussion_r1725070342> for more information.
pub channels: u8,
/// The transformation required to orient the image correctly.
///
/// This will be [`OrientationValue::Horizontal`] after the orientation step is applied.
pub orientation: OrientationValue,
}
#[allow(dead_code)]
#[derive(Tag)]
struct ArwIfd {
image_width: ImageWidth,
image_height: ImageLength,
compression: Compression,
strip_byte_counts: StripByteCounts,
}
impl RawImage {
/// Create a [`RawImage`] from an input stream.
///
/// Decodes the contents of `reader` and extracts raw pixel data and metadata.
pub fn decode<R: Read + Seek>(reader: &mut R) -> Result<RawImage, DecoderError> {
let mut file = TiffRead::new(reader)?;
let ifd = Ifd::new_first_ifd(&mut file)?;
let camera_model = metadata::identify::identify_camera_model(&ifd, &mut file).unwrap();
let orientation = ifd.get_value::<Orientation, _>(&mut file)?;
let mut raw_image = if camera_model.model == "DSLR-A100" {
decoder::arw1::decode_a100(ifd, &mut file)
} else {
let sub_ifd = ifd.get_value::<SubIfd, _>(&mut file)?;
let arw_ifd = sub_ifd.get_value::<ArwIfd, _>(&mut file)?;
if arw_ifd.compression == CompressionValue::Uncompressed {
decoder::uncompressed::decode(sub_ifd, &mut file)
} else if arw_ifd.strip_byte_counts[0] == arw_ifd.image_width * arw_ifd.image_height {
decoder::arw2::decode(sub_ifd, &mut file)
} else {
// TODO: implement for arw 1.
todo!()
}
};
raw_image.camera_model = Some(camera_model);
raw_image.orientation = orientation;
raw_image.calculate_conversion_matrices();
Ok(raw_image)
}
/// Extracts the thumbnail image from the raw file.
pub fn extract_thumbnail<R: Read + Seek>(reader: &mut R) -> Result<ThumbnailImage, DecoderError> {
let mut file = TiffRead::new(reader)?;
let ifd = Ifd::new_first_ifd(&mut file)?;
// TODO: ARW files Store the thumbnail offset and length in the first IFD. Add support for other file types in the future.
let thumbnail_offset = ifd.get_value::<ThumbnailOffset, _>(&mut file)?;
let thumbnail_length = ifd.get_value::<ThumbnailLength, _>(&mut file)?;
file.seek_from_start(thumbnail_offset)?;
let mut thumbnail_data = vec![0; thumbnail_length as usize];
file.read_exact(&mut thumbnail_data)?;
// Check the first two bytes to determine the format of the thumbnail.
// JPEG format starts with 0xFF, 0xD8.
if thumbnail_data[0..2] == [0xFF, 0xD8] {
Ok(ThumbnailImage {
data: thumbnail_data,
format: ThumbnailFormat::Jpeg,
})
} else {
Err(DecoderError::UnsupportedThumbnailFormat)
}
}
/// Converts the [`RawImage`] to an [`Image`] with 8 bit resolution for each channel.
///
/// Applies all the processing steps to finally get RGB pixel data.
pub fn process_8bit(self) -> Image<u8> {
let image = self.process_16bit();
Image {
channels: image.channels,
data: image.data.iter().map(|x| (x >> 8) as u8).collect(),
width: image.width,
height: image.height,
orientation: image.orientation,
}
}
/// Converts the [`RawImage`] to an [`Image`] with 16 bit resolution for each channel.
///
/// Applies all the processing steps to finally get RGB pixel data.
pub fn process_16bit(self) -> Image<u16> {
let subtract_black = self.subtract_black_fn();
let scale_white_balance = self.scale_white_balance_fn();
let scale_to_16bit = self.scale_to_16bit_fn();
let raw_image = self.apply((subtract_black, scale_white_balance, scale_to_16bit));
let convert_to_rgb = raw_image.convert_to_rgb_fn();
let mut record_histogram = raw_image.record_histogram_fn();
let image = raw_image.demosaic_and_apply((convert_to_rgb, &mut record_histogram));
let gamma_correction = image.gamma_correction_fn(&record_histogram.histogram);
if image.orientation == OrientationValue::Horizontal {
image.apply(gamma_correction)
} else {
image.transform_and_apply(gamma_correction)
}
}
}
impl RawImage {
pub fn apply(mut self, mut transform: impl RawPixelTransform) -> RawImage {
for (index, value) in self.data.iter_mut().enumerate() {
let pixel = RawPixel {
value: *value,
row: index / self.width,
column: index % self.width,
};
*value = transform.apply(pixel);
}
self
}
pub fn demosaic_and_apply(self, mut transform: impl PixelTransform) -> Image<u16> {
let mut image = vec![0; self.width * self.height * 3];
for Pixel { values, row, column } in self.linear_demosaic_iter().map(|mut pixel| {
pixel.values = transform.apply(pixel);
pixel
}) {
let pixel_index = row * self.width + column;
image[3 * pixel_index..3 * (pixel_index + 1)].copy_from_slice(&values);
}
Image {
channels: 3,
data: image,
width: self.width,
height: self.height,
orientation: self.orientation,
}
}
}
impl Image<u16> {
pub fn apply(mut self, mut transform: impl PixelTransform) -> Image<u16> {
for (index, values) in self.data.chunks_exact_mut(3).enumerate() {
let pixel = Pixel {
values: values.try_into().unwrap(),
row: index / self.width,
column: index % self.width,
};
values.copy_from_slice(&transform.apply(pixel));
}
self
}
pub fn transform_and_apply(self, mut transform: impl PixelTransform) -> Image<u16> {
let mut image = vec![0; self.width * self.height * 3];
let (width, height, iter) = self.orientation_iter();
for Pixel { values, row, column } in iter.map(|mut pixel| {
pixel.values = transform.apply(pixel);
pixel
}) {
let pixel_index = row * width + column;
image[3 * pixel_index..3 * (pixel_index + 1)].copy_from_slice(&values);
}
Image {
channels: 3,
data: image,
width,
height,
orientation: OrientationValue::Horizontal,
}
}
}
#[derive(Error, Debug)]
pub enum DecoderError {
#[error("An error occurred when trying to parse the TIFF format")]
TiffError(#[from] TiffError),
#[error("An error occurred when converting integer from one type to another")]
ConversionError(#[from] std::num::TryFromIntError),
#[error("An IO Error ocurred")]
IoError(#[from] std::io::Error),
#[error("The thumbnail format is unsupported")]
UnsupportedThumbnailFormat,
}
@@ -0,0 +1,118 @@
use crate::RawImage;
use rawkit_proc_macros::build_camera_data;
pub struct CameraData {
pub black: u16,
pub maximum: u16,
pub xyz_to_camera: [i16; 9],
}
impl CameraData {
const DEFAULT: CameraData = CameraData {
black: 0,
maximum: 0,
xyz_to_camera: [0; 9],
};
}
const CAMERA_DATA: [(&str, CameraData); 40] = build_camera_data!();
const RGB_TO_XYZ: [[f64; 3]; 3] = [
// Matrix:
[0.412453, 0.357580, 0.180423],
[0.212671, 0.715160, 0.072169],
[0.019334, 0.119193, 0.950227],
];
impl RawImage {
pub fn calculate_conversion_matrices(&mut self) {
let Some(ref camera_model) = self.camera_model else { return };
let camera_name_needle = camera_model.make.to_owned() + " " + &camera_model.model;
let xyz_to_camera = CAMERA_DATA
.iter()
.find(|(camera_name_haystack, _)| camera_name_needle == *camera_name_haystack)
.map(|(_, data)| data.xyz_to_camera.map(|x| (x as f64) / 10_000.));
let Some(xyz_to_camera) = xyz_to_camera else { return };
let mut rgb_to_camera = [[0.; 3]; 3];
for i in 0..3 {
for j in 0..3 {
for k in 0..3 {
rgb_to_camera[i][j] += RGB_TO_XYZ[k][j] * xyz_to_camera[i * 3 + k];
}
}
}
let white_balance_multiplier = rgb_to_camera.map(|x| 1. / x.iter().sum::<f64>());
for (index, row) in rgb_to_camera.iter_mut().enumerate() {
*row = row.map(|x| x * white_balance_multiplier[index]);
}
let camera_to_rgb = transpose(pseudoinverse(rgb_to_camera));
let cfa_white_balance_multiplier = if let Some(white_balance) = self.camera_white_balance {
white_balance
} else {
self.cfa_pattern.map(|index| white_balance_multiplier[index as usize])
};
self.white_balance = Some(cfa_white_balance_multiplier);
self.camera_to_rgb = Some(camera_to_rgb);
}
}
#[allow(clippy::needless_range_loop)]
fn pseudoinverse<const N: usize>(matrix: [[f64; 3]; N]) -> [[f64; 3]; N] {
let mut output_matrix = [[0.; 3]; N];
let mut work = [[0.; 6]; 3];
for i in 0..3 {
for j in 0..6 {
work[i][j] = if j == i + 3 { 1. } else { 0. };
}
for j in 0..3 {
for k in 0..N {
work[i][j] += matrix[k][i] * matrix[k][j];
}
}
}
for i in 0..3 {
let num = work[i][i];
for j in 0..6 {
work[i][j] /= num;
}
for k in 0..3 {
if k == i {
continue;
}
let num = work[k][i];
for j in 0..6 {
work[k][j] -= work[i][j] * num;
}
}
}
for i in 0..N {
for j in 0..3 {
output_matrix[i][j] = 0.;
for k in 0..3 {
output_matrix[i][j] += work[j][k + 3] * matrix[i][k];
}
}
}
output_matrix
}
fn transpose<const N: usize>(matrix: [[f64; 3]; N]) -> [[f64; N]; 3] {
let mut output_matrix = [[0.; N]; 3];
for (i, row) in matrix.iter().enumerate() {
for (j, &value) in row.iter().enumerate() {
output_matrix[j][i] = value;
}
}
output_matrix
}
@@ -0,0 +1,59 @@
use crate::tiff::file::TiffRead;
use crate::tiff::tags::{Make, Model, Tag};
use crate::tiff::{Ifd, TiffError};
use rawkit_proc_macros::Tag;
use std::io::{Read, Seek};
const COMPANY_NAMES: [&str; 22] = [
"AgfaPhoto",
"Canon",
"Casio",
"Epson",
"Fujifilm",
"Mamiya",
"Minolta",
"Motorola",
"Kodak",
"Konica",
"Leica",
"Nikon",
"Nokia",
"Olympus",
"Ricoh",
"Pentax",
"Phase One",
"Samsung",
"Sigma",
"Sinar",
"Sony",
"YI",
];
#[allow(dead_code)]
#[derive(Tag)]
struct CameraModelIfd {
make: Make,
model: Model,
}
pub struct CameraModel {
pub make: String,
pub model: String,
}
pub fn identify_camera_model<R: Read + Seek>(ifd: &Ifd, file: &mut TiffRead<R>) -> Option<CameraModel> {
let mut ifd = ifd.get_value::<CameraModelIfd, _>(file).unwrap();
ifd.make.make_ascii_lowercase();
for company_name in COMPANY_NAMES {
let lowercase_company_name = company_name.to_ascii_lowercase();
if ifd.make.contains(&lowercase_company_name) {
return Some(CameraModel {
make: company_name.to_string(),
model: ifd.model,
});
}
}
None
}
@@ -0,0 +1,2 @@
pub mod camera_data;
pub mod identify;
@@ -0,0 +1,13 @@
use crate::{CHANNELS_IN_RGB, Pixel, RawImage};
impl RawImage {
pub fn convert_to_rgb_fn(&self) -> impl Fn(Pixel) -> [u16; CHANNELS_IN_RGB] + use<> {
let Some(camera_to_rgb) = self.camera_to_rgb else { todo!() };
move |pixel: Pixel| {
std::array::from_fn(|i| i)
.map(|i| camera_to_rgb[i].iter().zip(pixel.values.iter()).map(|(&coeff, &value)| coeff * value as f64).sum())
.map(|x: f64| (x as u16).clamp(0, u16::MAX))
}
}
}
@@ -0,0 +1,84 @@
use crate::{CHANNELS_IN_RGB, Histogram, Image, Pixel};
use std::f64::consts::E;
impl Image<u16> {
pub fn gamma_correction_fn(&self, histogram: &Histogram) -> impl Fn(Pixel) -> [u16; CHANNELS_IN_RGB] + use<> {
let percentage = self.width * self.height;
let mut white = 0;
for channel_histogram in histogram {
let mut total = 0;
for i in (0x20..0x2000).rev() {
total += channel_histogram[i] as u64;
if total * 100 > percentage as u64 {
white = white.max(i);
break;
}
}
}
let curve = generate_gamma_curve(0.45, 4.5, (white << 3) as f64);
move |pixel: Pixel| pixel.values.map(|value| curve[value as usize])
}
}
/// `max_intensity` must be non-zero.
fn generate_gamma_curve(power: f64, threshold: f64, max_intensity: f64) -> Vec<u16> {
debug_assert!(max_intensity != 0.);
let (mut bound_start, mut bound_end) = if threshold >= 1. { (0., 1.) } else { (1., 0.) };
let mut transition_point = 0.;
let mut transition_ratio = 0.;
let mut curve_adjustment = 0.;
if threshold != 0. && (threshold - 1.) * (power - 1.) <= 0. {
for _ in 0..48 {
transition_point = (bound_start + bound_end) / 2.;
if power != 0. {
let temp_transition_ratio = transition_point / threshold;
let exponential_power = temp_transition_ratio.powf(-power);
let normalized_exponential_power = (exponential_power - 1.) / power;
let comparison_result = normalized_exponential_power - (1. / transition_point);
let bound_to_update = if comparison_result > -1. { &mut bound_end } else { &mut bound_start };
*bound_to_update = transition_point;
} else {
let adjusted_transition_point = E.powf(1. - 1. / transition_point);
let transition_point_ratio = transition_point / adjusted_transition_point;
let bound_to_update = if transition_point_ratio < threshold { &mut bound_end } else { &mut bound_start };
*bound_to_update = transition_point;
}
}
transition_ratio = transition_point / threshold;
if power != 0. {
curve_adjustment = transition_point * ((1. / power) - 1.);
}
}
let mut curve = vec![0xffff; 0x1_0000];
let length = curve.len() as f64;
for (i, entry) in curve.iter_mut().enumerate() {
let ratio = (i as f64) / max_intensity;
if ratio < 1. {
let altered_ratio = if ratio < transition_ratio {
ratio * threshold
} else if power != 0. {
ratio.powf(power) * (1. + curve_adjustment) - curve_adjustment
} else {
ratio.ln() * transition_point + 1.
};
*entry = (length * altered_ratio) as u16;
}
}
curve
}
@@ -0,0 +1,4 @@
pub mod convert_to_rgb;
pub mod gamma_correction;
pub mod record_histogram;
pub mod transform;
@@ -0,0 +1,29 @@
use crate::{CHANNELS_IN_RGB, Histogram, Pixel, PixelTransform, RawImage};
impl RawImage {
pub fn record_histogram_fn(&self) -> RecordHistogram {
RecordHistogram::new()
}
}
pub struct RecordHistogram {
pub histogram: Histogram,
}
impl RecordHistogram {
fn new() -> RecordHistogram {
RecordHistogram {
histogram: [[0; 0x2000]; CHANNELS_IN_RGB],
}
}
}
impl PixelTransform for &mut RecordHistogram {
fn apply(&mut self, pixel: Pixel) -> [u16; CHANNELS_IN_RGB] {
self.histogram
.iter_mut()
.zip(pixel.values.iter())
.for_each(|(histogram, &value)| histogram[value as usize >> CHANNELS_IN_RGB] += 1);
pixel.values
}
}
@@ -0,0 +1,70 @@
use crate::{Image, OrientationValue, Pixel};
impl Image<u16> {
pub fn orientation_iter(&self) -> (usize, usize, impl Iterator<Item = Pixel> + use<'_>) {
let (final_width, final_height) = if self.orientation.will_swap_coordinates() {
(self.height, self.width)
} else {
(self.width, self.height)
};
let index_0_0 = inverse_orientation_index(self.orientation, 0, 0, self.width, self.height);
let index_0_1 = inverse_orientation_index(self.orientation, 0, 1, self.width, self.height);
let index_1_0 = inverse_orientation_index(self.orientation, 1, 0, self.width, self.height);
let column_step = (index_0_1.0 - index_0_0.0, index_0_1.1 - index_0_0.1);
let row_step = (index_1_0.0 - index_0_0.0, index_1_0.1 - index_0_0.1);
let mut index = index_0_0;
let channels = self.channels as usize;
(
final_width,
final_height,
(0..final_height).flat_map(move |row| {
let temp = (0..final_width).map(move |column| {
let initial_index = (self.width as i64 * index.0 + index.1) as usize;
let pixel = &self.data[channels * initial_index..channels * (initial_index + 1)];
index = (index.0 + column_step.0, index.1 + column_step.1);
Pixel {
values: pixel.try_into().unwrap(),
row,
column,
}
});
index = (index.0 + row_step.0, index.1 + row_step.1);
temp
}),
)
}
}
pub fn inverse_orientation_index(orientation: OrientationValue, mut row: usize, mut column: usize, width: usize, height: usize) -> (i64, i64) {
let value = match orientation {
OrientationValue::Horizontal => 0,
OrientationValue::MirrorHorizontal => 1,
OrientationValue::Rotate180 => 3,
OrientationValue::MirrorVertical => 2,
OrientationValue::MirrorHorizontalRotate270 => 4,
OrientationValue::Rotate90 => 6,
OrientationValue::MirrorHorizontalRotate90 => 7,
OrientationValue::Rotate270 => 5,
};
if value & 4 != 0 {
std::mem::swap(&mut row, &mut column)
}
if value & 2 != 0 {
row = height - 1 - row;
}
if value & 1 != 0 {
column = width - 1 - column;
}
(row as i64, column as i64)
}
@@ -0,0 +1,3 @@
pub mod scale_to_16bit;
pub mod scale_white_balance;
pub mod subtract_black;
@@ -0,0 +1,15 @@
use crate::{RawImage, RawPixel, SubtractBlack};
impl RawImage {
pub fn scale_to_16bit_fn(&self) -> impl Fn(RawPixel) -> u16 + use<> {
let black_level = match self.black {
SubtractBlack::CfaGrid(x) => x,
_ => unreachable!(),
};
let maximum = self.maximum - black_level.iter().max().unwrap();
let scale_to_16bit_multiplier = if maximum > 0 { u16::MAX as f64 / maximum as f64 } else { 1. };
move |pixel: RawPixel| ((pixel.value as f64) * scale_to_16bit_multiplier).min(u16::MAX as f64).max(0.) as u16
}
}
@@ -0,0 +1,31 @@
use crate::{RawImage, RawPixel};
impl RawImage {
pub fn scale_white_balance_fn(&self) -> impl Fn(RawPixel) -> u16 + use<> {
let Some(mut white_balance) = self.white_balance else { todo!() };
if white_balance[1] == 0. {
white_balance[1] = 1.;
}
// TODO: Move this at its correct location when highlights are implemented correctly.
let highlight = 0;
let normalization_factor = if highlight == 0 {
white_balance.into_iter().fold(f64::INFINITY, f64::min)
} else {
white_balance.into_iter().fold(f64::NEG_INFINITY, f64::max)
};
let normalized_white_balance = if normalization_factor > 0.00001 {
white_balance.map(|x| x / normalization_factor)
} else {
[1., 1., 1., 1.]
};
move |pixel: RawPixel| {
let cfa_index = 2 * (pixel.row % 2) + (pixel.column % 2);
((pixel.value as f64) * normalized_white_balance[cfa_index]).min(u16::MAX as f64).max(0.) as u16
}
}
}
@@ -0,0 +1,11 @@
use crate::RawPixel;
use crate::{RawImage, SubtractBlack};
impl RawImage {
pub fn subtract_black_fn(&self) -> impl Fn(RawPixel) -> u16 + use<> {
match self.black {
SubtractBlack::CfaGrid(black_levels) => move |pixel: RawPixel| pixel.value.saturating_sub(black_levels[2 * (pixel.row % 2) + (pixel.column % 2)]),
_ => todo!(),
}
}
}
@@ -0,0 +1,83 @@
use crate::CHANNELS_IN_RGB;
#[derive(Clone, Copy)]
pub struct RawPixel {
pub value: u16,
pub row: usize,
pub column: usize,
}
#[derive(Clone, Copy)]
pub struct Pixel {
pub values: [u16; CHANNELS_IN_RGB],
pub row: usize,
pub column: usize,
}
pub trait RawPixelTransform {
fn apply(&mut self, pixel: RawPixel) -> u16;
}
impl<T: Fn(RawPixel) -> u16> RawPixelTransform for T {
fn apply(&mut self, pixel: RawPixel) -> u16 {
self(pixel)
}
}
macro_rules! impl_raw_pixel_transform {
($($idx:tt $t:tt),+) => {
impl<$($t,)+> RawPixelTransform for ($($t,)+)
where
$($t: RawPixelTransform,)+
{
fn apply(&mut self, mut pixel: RawPixel) -> u16 {
$(pixel.value = self.$idx.apply(pixel);)*
pixel.value
}
}
};
}
impl_raw_pixel_transform!(0 A);
impl_raw_pixel_transform!(0 A, 1 B);
impl_raw_pixel_transform!(0 A, 1 B, 2 C);
impl_raw_pixel_transform!(0 A, 1 B, 2 C, 3 D);
impl_raw_pixel_transform!(0 A, 1 B, 2 C, 3 D, 4 E);
impl_raw_pixel_transform!(0 A, 1 B, 2 C, 3 D, 4 E, 5 F);
impl_raw_pixel_transform!(0 A, 1 B, 2 C, 3 D, 4 E, 5 F, 6 G);
impl_raw_pixel_transform!(0 A, 1 B, 2 C, 3 D, 4 E, 5 F, 6 G, 7 H);
pub trait PixelTransform {
fn apply(&mut self, pixel: Pixel) -> [u16; CHANNELS_IN_RGB];
}
impl<T: Fn(Pixel) -> [u16; CHANNELS_IN_RGB]> PixelTransform for T {
fn apply(&mut self, pixel: Pixel) -> [u16; CHANNELS_IN_RGB] {
self(pixel)
}
}
macro_rules! impl_pixel_transform {
($($idx:tt $t:tt),+) => {
impl<$($t,)+> PixelTransform for ($($t,)+)
where
$($t: PixelTransform,)+
{
fn apply(&mut self, mut pixel: Pixel) -> [u16; CHANNELS_IN_RGB] {
$(pixel.values = self.$idx.apply(pixel);)*
pixel.values
}
}
};
}
impl_pixel_transform!(0 A);
impl_pixel_transform!(0 A, 1 B);
impl_pixel_transform!(0 A, 1 B, 2 C);
impl_pixel_transform!(0 A, 1 B, 2 C, 3 D);
impl_pixel_transform!(0 A, 1 B, 2 C, 3 D, 4 E);
impl_pixel_transform!(0 A, 1 B, 2 C, 3 D, 4 E, 5 F);
impl_pixel_transform!(0 A, 1 B, 2 C, 3 D, 4 E, 5 F, 6 G);
impl_pixel_transform!(0 A, 1 B, 2 C, 3 D, 4 E, 5 F, 6 G, 7 H);
@@ -0,0 +1,152 @@
use std::io::{Error, ErrorKind, Read, Result, Seek, SeekFrom};
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
pub enum Endian {
Little,
Big,
}
pub struct TiffRead<R: Read + Seek> {
reader: R,
endian: Endian,
}
impl<R: Read + Seek> TiffRead<R> {
pub fn new(mut reader: R) -> Result<Self> {
let error = Error::new(ErrorKind::InvalidData, "Invalid Tiff format");
let mut data = [0_u8; 2];
reader.read_exact(&mut data)?;
let endian = if data[0] == 0x49 && data[1] == 0x49 {
Endian::Little
} else if data[0] == 0x4d && data[1] == 0x4d {
Endian::Big
} else {
return Err(error);
};
reader.read_exact(&mut data)?;
let magic_number = match endian {
Endian::Little => u16::from_le_bytes(data),
Endian::Big => u16::from_be_bytes(data),
};
if magic_number != 42 {
return Err(error);
}
Ok(Self { reader, endian })
}
pub fn endian(&self) -> Endian {
self.endian
}
}
impl<R: Read + Seek> Read for TiffRead<R> {
fn read(&mut self, buf: &mut [u8]) -> Result<usize> {
self.reader.read(buf)
}
}
impl<R: Read + Seek> Seek for TiffRead<R> {
fn seek(&mut self, pos: SeekFrom) -> Result<u64> {
self.reader.seek(pos)
}
}
impl<R: Read + Seek> TiffRead<R> {
pub fn seek_from_start(&mut self, offset: u32) -> Result<u64> {
self.reader.seek(SeekFrom::Start(offset.into()))
}
pub fn read_ascii(&mut self) -> Result<char> {
let data = self.read_n::<1>()?;
Ok(data[0] as char)
}
pub fn read_n<const N: usize>(&mut self) -> Result<[u8; N]> {
let mut data = [0_u8; N];
self.read_exact(&mut data)?;
Ok(data)
}
pub fn read_u8(&mut self) -> Result<u8> {
let data = self.read_n()?;
match self.endian {
Endian::Little => Ok(u8::from_le_bytes(data)),
Endian::Big => Ok(u8::from_be_bytes(data)),
}
}
pub fn read_u16(&mut self) -> Result<u16> {
let data = self.read_n()?;
match self.endian {
Endian::Little => Ok(u16::from_le_bytes(data)),
Endian::Big => Ok(u16::from_be_bytes(data)),
}
}
pub fn read_u32(&mut self) -> Result<u32> {
let data = self.read_n()?;
match self.endian {
Endian::Little => Ok(u32::from_le_bytes(data)),
Endian::Big => Ok(u32::from_be_bytes(data)),
}
}
pub fn read_u64(&mut self) -> Result<u64> {
let data = self.read_n()?;
match self.endian {
Endian::Little => Ok(u64::from_le_bytes(data)),
Endian::Big => Ok(u64::from_be_bytes(data)),
}
}
pub fn read_i8(&mut self) -> Result<i8> {
let data = self.read_n()?;
match self.endian {
Endian::Little => Ok(i8::from_le_bytes(data)),
Endian::Big => Ok(i8::from_be_bytes(data)),
}
}
pub fn read_i16(&mut self) -> Result<i16> {
let data = self.read_n()?;
match self.endian {
Endian::Little => Ok(i16::from_le_bytes(data)),
Endian::Big => Ok(i16::from_be_bytes(data)),
}
}
pub fn read_i32(&mut self) -> Result<i32> {
let data = self.read_n()?;
match self.endian {
Endian::Little => Ok(i32::from_le_bytes(data)),
Endian::Big => Ok(i32::from_be_bytes(data)),
}
}
pub fn read_i64(&mut self) -> Result<i64> {
let data = self.read_n()?;
match self.endian {
Endian::Little => Ok(i64::from_le_bytes(data)),
Endian::Big => Ok(i64::from_be_bytes(data)),
}
}
pub fn read_f32(&mut self) -> Result<f32> {
let data = self.read_n()?;
match self.endian {
Endian::Little => Ok(f32::from_le_bytes(data)),
Endian::Big => Ok(f32::from_be_bytes(data)),
}
}
pub fn read_f64(&mut self) -> Result<f64> {
let data = self.read_n()?;
match self.endian {
Endian::Little => Ok(f64::from_le_bytes(data)),
Endian::Big => Ok(f64::from_be_bytes(data)),
}
}
}
@@ -0,0 +1,171 @@
pub mod file;
pub mod tags;
mod types;
pub mod values;
use file::TiffRead;
use num_enum::{FromPrimitive, IntoPrimitive};
use std::fmt::Display;
use std::io::{Read, Seek};
use tags::Tag;
use thiserror::Error;
#[derive(Copy, Clone, Debug, PartialEq, Eq, FromPrimitive, IntoPrimitive)]
#[repr(u16)]
pub enum TagId {
ImageWidth = 0x100,
ImageLength = 0x101,
BitsPerSample = 0x102,
Compression = 0x103,
PhotometricInterpretation = 0x104,
Make = 0x10f,
Model = 0x110,
StripOffsets = 0x111,
Orientation = 0x112,
SamplesPerPixel = 0x115,
RowsPerStrip = 0x116,
StripByteCounts = 0x117,
SubIfd = 0x14a,
ThumbnailOffset = 0x201,
ThumbnailLength = 0x202,
SonyToneCurve = 0x7010,
BlackLevel = 0x7310,
WhiteBalanceRggbLevels = 0x7313,
CfaPatternDim = 0x828d,
CfaPattern = 0x828e,
ColorMatrix1 = 0xc621,
ColorMatrix2 = 0xc622,
#[num_enum(catch_all)]
Unknown(u16),
}
#[repr(u16)]
#[derive(Copy, Clone, Debug, PartialEq, Eq, FromPrimitive, IntoPrimitive)]
pub enum IfdTagType {
Byte = 1,
Ascii = 2,
Short = 3,
Long = 4,
Rational = 5,
SByte = 6,
Undefined = 7,
SShort = 8,
SLong = 9,
SRational = 10,
Float = 11,
Double = 12,
#[num_enum(catch_all)]
Unknown(u16),
}
#[derive(Copy, Clone, Debug)]
pub struct IfdEntry {
tag: TagId,
the_type: IfdTagType,
count: u32,
value: u32,
}
#[derive(Clone, Debug)]
pub struct Ifd {
current_ifd_offset: u32,
ifd_entries: Vec<IfdEntry>,
next_ifd_offset: Option<u32>,
}
impl Ifd {
pub fn new_first_ifd<R: Read + Seek>(file: &mut TiffRead<R>) -> Result<Self, TiffError> {
file.seek_from_start(4)?;
let current_ifd_offset = file.read_u32()?;
Ifd::new_from_offset(file, current_ifd_offset)
}
pub fn new_from_offset<R: Read + Seek>(file: &mut TiffRead<R>, offset: u32) -> Result<Self, TiffError> {
if offset == 0 {
return Err(TiffError::InvalidOffset);
}
file.seek_from_start(offset)?;
let num_entries = file.read_u16()?;
let mut ifd_entries = Vec::with_capacity(num_entries.into());
for _ in 0..num_entries {
let tag = file.read_u16()?.into();
let the_type = file.read_u16()?.into();
let count = file.read_u32()?;
let value = file.read_u32()?;
ifd_entries.push(IfdEntry { tag, the_type, count, value });
}
let next_ifd_offset = file.read_u32()?;
let next_ifd_offset = if next_ifd_offset == 0 { None } else { Some(next_ifd_offset) };
Ok(Ifd {
current_ifd_offset: offset,
ifd_entries,
next_ifd_offset,
})
}
fn _next_ifd<R: Read + Seek>(&self, file: &mut TiffRead<R>) -> Result<Self, TiffError> {
Ifd::new_from_offset(file, self.next_ifd_offset.unwrap_or(0))
}
pub fn ifd_entries(&self) -> &[IfdEntry] {
&self.ifd_entries
}
pub fn iter(&self) -> impl Iterator<Item = &IfdEntry> {
self.ifd_entries.iter()
}
pub fn get_value<T: Tag, R: Read + Seek>(&self, file: &mut TiffRead<R>) -> Result<T::Output, TiffError> {
T::get(self, file)
}
}
impl Display for Ifd {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str("IFD offset: ")?;
self.current_ifd_offset.fmt(f)?;
f.write_str("\n")?;
for ifd_entry in self.ifd_entries() {
f.write_fmt(format_args!(
"|- Tag: {:x?}, Type: {:?}, Count: {}, Value: {:x}\n",
ifd_entry.tag, ifd_entry.the_type, ifd_entry.count, ifd_entry.value
))?;
}
f.write_str("Next IFD offset: ")?;
if let Some(offset) = self.next_ifd_offset {
offset.fmt(f)?;
} else {
f.write_str("None")?;
}
f.write_str("\n")?;
Ok(())
}
}
#[derive(Error, Debug)]
pub enum TiffError {
#[error("The value was invalid")]
InvalidValue,
#[error("The type was invalid")]
InvalidType,
#[error("The count was invalid")]
InvalidCount,
#[error("The tag was missing")]
MissingTag,
#[error("The offset was invalid or zero")]
InvalidOffset,
#[error("An error occurred when converting integer from one type to another")]
ConversionError(#[from] std::num::TryFromIntError),
#[error("An IO Error ocurred")]
IoError(#[from] std::io::Error),
}
@@ -0,0 +1,227 @@
use super::types::{Array, ConstArray, TagType, TypeByte, TypeCompression, TypeIfd, TypeLong, TypeNumber, TypeOrientation, TypeSRational, TypeSShort, TypeShort, TypeSonyToneCurve, TypeString};
use super::{Ifd, TagId, TiffError, TiffRead};
use std::io::{Read, Seek};
pub trait SimpleTag {
type Type: TagType;
const ID: TagId;
const NAME: &'static str;
}
pub struct ImageWidth;
pub struct ImageLength;
pub struct BitsPerSample;
pub struct Compression;
pub struct PhotometricInterpretation;
pub struct Make;
pub struct Model;
pub struct StripOffsets;
pub struct Orientation;
pub struct SamplesPerPixel;
pub struct RowsPerStrip;
pub struct StripByteCounts;
pub struct SubIfd;
pub struct ThumbnailOffset;
pub struct ThumbnailLength;
pub struct SonyDataOffset;
pub struct SonyToneCurve;
pub struct BlackLevel;
pub struct WhiteBalanceRggbLevels;
pub struct CfaPatternDim;
pub struct CfaPattern;
pub struct ColorMatrix1;
pub struct ColorMatrix2;
impl SimpleTag for ImageWidth {
type Type = TypeNumber;
const ID: TagId = TagId::ImageWidth;
const NAME: &'static str = "Image Width";
}
impl SimpleTag for ImageLength {
type Type = TypeNumber;
const ID: TagId = TagId::ImageLength;
const NAME: &'static str = "Image Length";
}
impl SimpleTag for BitsPerSample {
type Type = TypeShort;
const ID: TagId = TagId::BitsPerSample;
const NAME: &'static str = "Bits per Sample";
}
impl SimpleTag for Compression {
type Type = TypeCompression;
const ID: TagId = TagId::Compression;
const NAME: &'static str = "Compression";
}
impl SimpleTag for PhotometricInterpretation {
type Type = TypeShort;
const ID: TagId = TagId::PhotometricInterpretation;
const NAME: &'static str = "Photometric Interpretation";
}
impl SimpleTag for Make {
type Type = TypeString;
const ID: TagId = TagId::Make;
const NAME: &'static str = "Make";
}
impl SimpleTag for Model {
type Type = TypeString;
const ID: TagId = TagId::Model;
const NAME: &'static str = "Model";
}
impl SimpleTag for StripOffsets {
type Type = Array<TypeNumber>;
const ID: TagId = TagId::StripOffsets;
const NAME: &'static str = "Strip Offsets";
}
impl SimpleTag for Orientation {
type Type = TypeOrientation;
const ID: TagId = TagId::Orientation;
const NAME: &'static str = "Orientation";
}
impl SimpleTag for SamplesPerPixel {
type Type = TypeShort;
const ID: TagId = TagId::SamplesPerPixel;
const NAME: &'static str = "Samples per Pixel";
}
impl SimpleTag for RowsPerStrip {
type Type = TypeNumber;
const ID: TagId = TagId::RowsPerStrip;
const NAME: &'static str = "Rows per Strip";
}
impl SimpleTag for StripByteCounts {
type Type = Array<TypeNumber>;
const ID: TagId = TagId::StripByteCounts;
const NAME: &'static str = "Strip Byte Counts";
}
impl SimpleTag for SubIfd {
type Type = TypeIfd;
const ID: TagId = TagId::SubIfd;
const NAME: &'static str = "SubIFD";
}
impl SimpleTag for ThumbnailOffset {
type Type = TypeLong;
const ID: TagId = TagId::ThumbnailOffset;
const NAME: &'static str = "Jpeg Offset";
}
impl SimpleTag for ThumbnailLength {
type Type = TypeLong;
const ID: TagId = TagId::ThumbnailLength;
const NAME: &'static str = "Jpeg Length";
}
impl SimpleTag for CfaPatternDim {
type Type = ConstArray<TypeShort, 2>;
const ID: TagId = TagId::CfaPatternDim;
const NAME: &'static str = "CFA Pattern Dimension";
}
impl SimpleTag for CfaPattern {
type Type = Array<TypeByte>;
const ID: TagId = TagId::CfaPattern;
const NAME: &'static str = "CFA Pattern";
}
impl SimpleTag for ColorMatrix1 {
type Type = Array<TypeSRational>;
const ID: TagId = TagId::ColorMatrix1;
const NAME: &'static str = "Color Matrix 1";
}
impl SimpleTag for ColorMatrix2 {
type Type = Array<TypeSRational>;
const ID: TagId = TagId::ColorMatrix2;
const NAME: &'static str = "Color Matrix 2";
}
impl SimpleTag for SonyDataOffset {
type Type = TypeLong;
const ID: TagId = TagId::SubIfd;
const NAME: &'static str = "Sony Data Offset";
}
impl SimpleTag for SonyToneCurve {
type Type = TypeSonyToneCurve;
const ID: TagId = TagId::SonyToneCurve;
const NAME: &'static str = "Sony Tone Curve";
}
impl SimpleTag for BlackLevel {
type Type = ConstArray<TypeShort, 4>;
const ID: TagId = TagId::BlackLevel;
const NAME: &'static str = "Black Level";
}
impl SimpleTag for WhiteBalanceRggbLevels {
type Type = ConstArray<TypeSShort, 4>;
const ID: TagId = TagId::WhiteBalanceRggbLevels;
const NAME: &'static str = "White Balance Levels (RGGB)";
}
pub trait Tag {
type Output;
fn get<R: Read + Seek>(ifd: &Ifd, file: &mut TiffRead<R>) -> Result<Self::Output, TiffError>;
}
impl<T: SimpleTag> Tag for T {
type Output = <T::Type as TagType>::Output;
fn get<R: Read + Seek>(ifd: &Ifd, file: &mut TiffRead<R>) -> Result<Self::Output, TiffError> {
let tag_id = T::ID;
let index: u32 = ifd.iter().position(|x| x.tag == tag_id).ok_or(TiffError::MissingTag)?.try_into()?;
file.seek_from_start(ifd.current_ifd_offset + 2 + 12 * index + 2)?;
T::Type::read(file)
}
}
impl<T: Tag> Tag for Option<T> {
type Output = Option<T::Output>;
fn get<R: Read + Seek>(ifd: &Ifd, file: &mut TiffRead<R>) -> Result<Self::Output, TiffError> {
let result = T::get(ifd, file);
match result {
Err(TiffError::MissingTag) => Ok(None),
Ok(x) => Ok(Some(x)),
Err(x) => Err(x),
}
}
}
@@ -0,0 +1,393 @@
use super::file::TiffRead;
use super::values::{CompressionValue, CurveLookupTable, OrientationValue, Rational};
use super::{Ifd, IfdTagType, TiffError};
use std::io::{Read, Seek};
pub struct TypeAscii;
pub struct TypeByte;
pub struct TypeShort;
pub struct TypeLong;
pub struct TypeRational;
pub struct TypeSByte;
pub struct TypeSShort;
pub struct TypeSLong;
pub struct TypeSRational;
pub struct TypeFloat;
pub struct TypeDouble;
pub struct TypeUndefined;
pub struct TypeNumber;
pub struct TypeSNumber;
pub struct TypeIfd;
pub trait PrimitiveType {
type Output;
fn get_size(the_type: IfdTagType) -> Option<u32>;
fn read_primitive<R: Read + Seek>(the_type: IfdTagType, file: &mut TiffRead<R>) -> Result<Self::Output, TiffError>;
}
impl PrimitiveType for TypeAscii {
type Output = char;
fn get_size(the_type: IfdTagType) -> Option<u32> {
match the_type {
IfdTagType::Ascii => Some(1),
_ => None,
}
}
fn read_primitive<R: Read + Seek>(_: IfdTagType, file: &mut TiffRead<R>) -> Result<Self::Output, TiffError> {
let value = file.read_ascii()?;
if value.is_ascii() { Ok(value) } else { Err(TiffError::InvalidValue) }
}
}
impl PrimitiveType for TypeByte {
type Output = u8;
fn get_size(the_type: IfdTagType) -> Option<u32> {
match the_type {
IfdTagType::Byte => Some(1),
_ => None,
}
}
fn read_primitive<R: Read + Seek>(_: IfdTagType, file: &mut TiffRead<R>) -> Result<Self::Output, TiffError> {
Ok(file.read_u8()?)
}
}
impl PrimitiveType for TypeShort {
type Output = u16;
fn get_size(the_type: IfdTagType) -> Option<u32> {
match the_type {
IfdTagType::Short => Some(2),
_ => None,
}
}
fn read_primitive<R: Read + Seek>(_: IfdTagType, file: &mut TiffRead<R>) -> Result<Self::Output, TiffError> {
Ok(file.read_u16()?)
}
}
impl PrimitiveType for TypeLong {
type Output = u32;
fn get_size(the_type: IfdTagType) -> Option<u32> {
match the_type {
IfdTagType::Long => Some(4),
_ => None,
}
}
fn read_primitive<R: Read + Seek>(_: IfdTagType, file: &mut TiffRead<R>) -> Result<Self::Output, TiffError> {
Ok(file.read_u32()?)
}
}
impl PrimitiveType for TypeRational {
type Output = Rational<u32>;
fn get_size(the_type: IfdTagType) -> Option<u32> {
match the_type {
IfdTagType::Rational => Some(8),
_ => None,
}
}
fn read_primitive<R: Read + Seek>(the_type: IfdTagType, file: &mut TiffRead<R>) -> Result<Self::Output, TiffError> {
let numerator = TypeLong::read_primitive(the_type, file)?;
let denominator = TypeLong::read_primitive(the_type, file)?;
Ok(Rational { numerator, denominator })
}
}
impl PrimitiveType for TypeSByte {
type Output = i8;
fn get_size(the_type: IfdTagType) -> Option<u32> {
match the_type {
IfdTagType::SByte => Some(1),
_ => None,
}
}
fn read_primitive<R: Read + Seek>(_: IfdTagType, file: &mut TiffRead<R>) -> Result<Self::Output, TiffError> {
Ok(file.read_i8()?)
}
}
impl PrimitiveType for TypeSShort {
type Output = i16;
fn get_size(the_type: IfdTagType) -> Option<u32> {
match the_type {
IfdTagType::SShort => Some(2),
_ => None,
}
}
fn read_primitive<R: Read + Seek>(_: IfdTagType, file: &mut TiffRead<R>) -> Result<Self::Output, TiffError> {
Ok(file.read_i16()?)
}
}
impl PrimitiveType for TypeSLong {
type Output = i32;
fn get_size(the_type: IfdTagType) -> Option<u32> {
match the_type {
IfdTagType::SLong => Some(4),
_ => None,
}
}
fn read_primitive<R: Read + Seek>(_: IfdTagType, file: &mut TiffRead<R>) -> Result<Self::Output, TiffError> {
Ok(file.read_i32()?)
}
}
impl PrimitiveType for TypeSRational {
type Output = Rational<i32>;
fn get_size(the_type: IfdTagType) -> Option<u32> {
match the_type {
IfdTagType::SRational => Some(8),
_ => None,
}
}
fn read_primitive<R: Read + Seek>(the_type: IfdTagType, file: &mut TiffRead<R>) -> Result<Self::Output, TiffError> {
let numerator = TypeSLong::read_primitive(the_type, file)?;
let denominator = TypeSLong::read_primitive(the_type, file)?;
Ok(Rational { numerator, denominator })
}
}
impl PrimitiveType for TypeFloat {
type Output = f32;
fn get_size(the_type: IfdTagType) -> Option<u32> {
match the_type {
IfdTagType::Float => Some(4),
_ => None,
}
}
fn read_primitive<R: Read + Seek>(_: IfdTagType, file: &mut TiffRead<R>) -> Result<Self::Output, TiffError> {
Ok(file.read_f32()?)
}
}
impl PrimitiveType for TypeDouble {
type Output = f64;
fn get_size(the_type: IfdTagType) -> Option<u32> {
match the_type {
IfdTagType::Double => Some(8),
_ => None,
}
}
fn read_primitive<R: Read + Seek>(_: IfdTagType, file: &mut TiffRead<R>) -> Result<Self::Output, TiffError> {
Ok(file.read_f64()?)
}
}
impl PrimitiveType for TypeUndefined {
type Output = ();
fn get_size(_: IfdTagType) -> Option<u32> {
todo!()
}
fn read_primitive<R: Read + Seek>(_: IfdTagType, _: &mut TiffRead<R>) -> Result<Self::Output, TiffError> {
todo!()
}
}
impl PrimitiveType for TypeNumber {
type Output = u32;
fn get_size(the_type: IfdTagType) -> Option<u32> {
match the_type {
IfdTagType::Byte => TypeByte::get_size(the_type),
IfdTagType::Short => TypeShort::get_size(the_type),
IfdTagType::Long => TypeLong::get_size(the_type),
_ => None,
}
}
fn read_primitive<R: Read + Seek>(the_type: IfdTagType, file: &mut TiffRead<R>) -> Result<Self::Output, TiffError> {
Ok(match the_type {
IfdTagType::Byte => TypeByte::read_primitive(the_type, file)?.into(),
IfdTagType::Short => TypeShort::read_primitive(the_type, file)?.into(),
IfdTagType::Long => TypeLong::read_primitive(the_type, file)?,
_ => unreachable!(),
})
}
}
impl PrimitiveType for TypeSNumber {
type Output = i32;
fn get_size(the_type: IfdTagType) -> Option<u32> {
match the_type {
IfdTagType::SByte => TypeSByte::get_size(the_type),
IfdTagType::SShort => TypeSShort::get_size(the_type),
IfdTagType::SLong => TypeSLong::get_size(the_type),
_ => None,
}
}
fn read_primitive<R: Read + Seek>(the_type: IfdTagType, file: &mut TiffRead<R>) -> Result<Self::Output, TiffError> {
Ok(match the_type {
IfdTagType::SByte => TypeSByte::read_primitive(the_type, file)?.into(),
IfdTagType::SShort => TypeSShort::read_primitive(the_type, file)?.into(),
IfdTagType::SLong => TypeSLong::read_primitive(the_type, file)?,
_ => unreachable!(),
})
}
}
impl PrimitiveType for TypeIfd {
type Output = Ifd;
fn get_size(the_type: IfdTagType) -> Option<u32> {
TypeLong::get_size(the_type)
}
fn read_primitive<R: Read + Seek>(the_type: IfdTagType, file: &mut TiffRead<R>) -> Result<Self::Output, TiffError> {
let offset = TypeLong::read_primitive(the_type, file)?;
Ifd::new_from_offset(file, offset)
}
}
pub trait TagType {
type Output;
fn read<R: Read + Seek>(file: &mut TiffRead<R>) -> Result<Self::Output, TiffError>;
}
impl<T: PrimitiveType> TagType for T {
type Output = T::Output;
fn read<R: Read + Seek>(file: &mut TiffRead<R>) -> Result<Self::Output, TiffError> {
let the_type = IfdTagType::from(file.read_u16()?);
let count = file.read_u32()?;
if count != 1 {
return Err(TiffError::InvalidCount);
}
let size = T::get_size(the_type).ok_or(TiffError::InvalidType)?;
if count * size > 4 {
let offset = file.read_u32()?;
file.seek_from_start(offset)?;
}
T::read_primitive(the_type, file)
}
}
pub struct Array<T: PrimitiveType> {
primitive_type: std::marker::PhantomData<T>,
}
pub struct ConstArray<T: PrimitiveType, const N: usize> {
primitive_type: std::marker::PhantomData<T>,
}
impl<T: PrimitiveType> TagType for Array<T> {
type Output = Vec<T::Output>;
fn read<R: Read + Seek>(file: &mut TiffRead<R>) -> Result<Self::Output, TiffError> {
let the_type = IfdTagType::from(file.read_u16()?);
let count = file.read_u32()?;
let size = T::get_size(the_type).ok_or(TiffError::InvalidType)?;
if count * size > 4 {
let offset = file.read_u32()?;
file.seek_from_start(offset)?;
}
let mut ans = Vec::with_capacity(count.try_into()?);
for _ in 0..count {
ans.push(T::read_primitive(the_type, file)?);
}
Ok(ans)
}
}
impl<T: PrimitiveType, const N: usize> TagType for ConstArray<T, N> {
type Output = [T::Output; N];
fn read<R: Read + Seek>(file: &mut TiffRead<R>) -> Result<Self::Output, TiffError> {
let the_type = IfdTagType::from(file.read_u16()?);
let count = file.read_u32()?;
if count != N.try_into()? {
return Err(TiffError::InvalidCount);
}
let size = T::get_size(the_type).ok_or(TiffError::InvalidType)?;
if count * size > 4 {
let offset = file.read_u32()?;
file.seek_from_start(offset)?;
}
let mut ans = Vec::with_capacity(count.try_into()?);
for _ in 0..count {
ans.push(T::read_primitive(the_type, file)?);
}
ans.try_into().map_err(|_| TiffError::InvalidCount)
}
}
pub struct TypeCompression;
pub struct TypeString;
pub struct TypeSonyToneCurve;
pub struct TypeOrientation;
impl TagType for TypeString {
type Output = String;
fn read<R: Read + Seek>(file: &mut TiffRead<R>) -> Result<Self::Output, TiffError> {
let string = Array::<TypeAscii>::read(file)?;
// Skip the NUL character at the end
let len = string.len();
Ok(string.into_iter().take(len - 1).collect())
}
}
impl TagType for TypeSonyToneCurve {
type Output = CurveLookupTable;
fn read<R: Read + Seek>(file: &mut TiffRead<R>) -> Result<Self::Output, TiffError> {
let values = ConstArray::<TypeShort, 4>::read(file)?;
Ok(CurveLookupTable::from_sony_tone_table(values))
}
}
impl TagType for TypeOrientation {
type Output = OrientationValue;
fn read<R: Read + Seek>(file: &mut TiffRead<R>) -> Result<Self::Output, TiffError> {
OrientationValue::try_from(TypeShort::read(file)?).map_err(|_| TiffError::InvalidValue)
}
}
impl TagType for TypeCompression {
type Output = CompressionValue;
fn read<R: Read + Seek>(file: &mut TiffRead<R>) -> Result<Self::Output, TiffError> {
CompressionValue::try_from(TypeShort::read(file)?).map_err(|_| TiffError::InvalidValue)
}
}
@@ -0,0 +1,132 @@
use num_enum::{IntoPrimitive, TryFromPrimitive};
pub trait ToFloat {
fn to_float(&self) -> f64;
}
impl ToFloat for u32 {
fn to_float(&self) -> f64 {
*self as f64
}
}
impl ToFloat for i32 {
fn to_float(&self) -> f64 {
*self as f64
}
}
pub struct Rational<T: ToFloat> {
pub numerator: T,
pub denominator: T,
}
impl<T: ToFloat> ToFloat for Rational<T> {
fn to_float(&self) -> f64 {
self.numerator.to_float() / self.denominator.to_float()
}
}
pub struct CurveLookupTable {
table: Vec<u16>,
}
impl CurveLookupTable {
pub fn from_sony_tone_table(values: [u16; 4]) -> CurveLookupTable {
let mut sony_curve = [0, 0, 0, 0, 0, 4095];
for i in 0..4 {
sony_curve[i + 1] = values[i] >> 2 & 0xfff;
}
let mut table = vec![0_u16; (sony_curve[5] + 1).into()];
for i in 0..5 {
for j in (sony_curve[i] + 1)..=sony_curve[i + 1] {
table[j as usize] = table[(j - 1) as usize] + (1 << i);
}
}
CurveLookupTable { table }
}
pub fn get(&self, x: usize) -> u16 {
self.table[x]
}
}
#[derive(Copy, Clone, Eq, PartialEq, Debug, IntoPrimitive, TryFromPrimitive)]
#[repr(u16)]
pub enum OrientationValue {
Horizontal = 1,
MirrorHorizontal = 2,
Rotate180 = 3,
MirrorVertical = 4,
MirrorHorizontalRotate270 = 5,
Rotate90 = 6,
MirrorHorizontalRotate90 = 7,
Rotate270 = 8,
}
impl OrientationValue {
pub fn is_identity(&self) -> bool {
*self == Self::Horizontal
}
pub fn will_swap_coordinates(&self) -> bool {
match *self {
Self::Horizontal | Self::MirrorHorizontal | Self::Rotate180 | Self::MirrorVertical => false,
Self::MirrorHorizontalRotate270 | Self::Rotate90 | Self::MirrorHorizontalRotate90 | Self::Rotate270 => true,
}
}
}
#[derive(Debug, PartialEq, Eq, Clone, Copy, IntoPrimitive, TryFromPrimitive)]
#[repr(u16)]
#[allow(non_camel_case_types)]
pub enum CompressionValue {
Uncompressed = 1,
CCITT_1D = 2,
T4 = 3,
T6 = 4,
LZW = 5,
JPEG_Old = 6,
JPEG = 7,
AdobeDeflate = 8,
JBIG_BW = 9,
JBIG_Color = 10,
KODAK_626 = 262,
Next = 32766,
Sony_ARW_Compressed = 32767,
Packed_Raw = 32769,
Samsung_SRW_Compressed = 32770,
CCIRLEW = 32771,
Samsung_SRW_Compressed_2 = 32772,
PackedBits = 32773,
Thunderscan = 32809,
Kodak_KDC_Compressed = 32867,
IT8CTPAD = 32895,
IT8LW = 32896,
IT8MP = 32897,
IT8BL = 32898,
PixarFilm = 32908,
PixarLog = 32909,
Deflate = 32946,
DCS = 32947,
AperioJPEG2K_YCbCr = 33003,
AperioJPEG2K_RGB = 33005,
JBIG = 34661,
SGILog = 34676,
SGILog24 = 34677,
JPEG2K = 34712,
NikonNEFCompressed = 34713,
JBIG2_TIFF_FX = 34715,
ESRI_Lerc = 34887,
LossyJPEG = 34892,
LZMA2 = 34925,
PNG = 34933,
JPEG_XR = 34934,
Zstd = 50000,
WebP = 50001,
JPEG_XL = 52546,
Kodak_DCR_Compressed = 65000,
Pentax_PEF_Compressed = 65535,
}
@@ -0,0 +1 @@
@@ -0,0 +1,364 @@
// Only compile this file if the feature "rawkit-tests" is enabled
#![cfg(feature = "rawkit-tests")]
use image::codecs::png::{CompressionType, FilterType, PngEncoder};
use image::{ColorType, ImageEncoder};
use libraw::Processor;
use rawkit::RawImage;
use rayon::prelude::*;
use std::collections::HashMap;
use std::fmt::Write;
use std::fs::{File, create_dir, metadata, read_dir};
use std::io::{BufWriter, Cursor, Read};
use std::path::{Path, PathBuf};
use std::sync::atomic::{AtomicUsize, Ordering};
use std::time::Duration;
const TEST_FILES: [&str; 3] = ["ILCE-7M3-ARW2.3.5-blossoms.arw", "ILCE-7RM4-ARW2.3.5-kestrel.arw", "ILCE-6000-ARW2.3.1-windsock.arw"];
const BASE_URL: &str = "https://static.graphite.art/test-data/libraries/rawkit/";
const BASE_PATH: &str = "./tests/images/";
#[test]
fn test_images_match_with_libraw() {
download_images();
let paths: Vec<_> = read_dir(BASE_PATH)
.unwrap()
.map(|dir_entry| dir_entry.unwrap().path())
.filter(|path| path.is_file() && path.file_name().map(|file_name| file_name != ".gitkeep").unwrap_or(false))
.collect();
let failed_tests = if std::env::var("RAWKIT_TEST_RUN_SEQUENTIALLY").is_ok() {
let mut failed_tests = 0;
paths.iter().for_each(|path| {
if !test_image(path) {
failed_tests += 1;
}
});
failed_tests
} else {
let failed_tests = AtomicUsize::new(0);
paths.par_iter().for_each(|path| {
if !test_image(path) {
failed_tests.fetch_add(1, Ordering::SeqCst);
}
});
failed_tests.load(Ordering::SeqCst)
};
if failed_tests != 0 {
panic!("{} images have failed the tests", failed_tests);
}
}
fn test_image(path: &Path) -> bool {
let mut f = File::open(path).unwrap();
let mut content = vec![];
f.read_to_end(&mut content).unwrap();
let raw_image = match test_raw_data(&content) {
Err(err_msg) => {
println!("{} => {}", path.display(), err_msg);
return false;
}
Ok(raw_image) => raw_image,
};
// TODO: The code below is kept commented because raw data to final image processing is
// incomplete. Remove this once it is done.
// if let Err(err_msg) = test_final_image(&content, raw_image) {
// failed_tests += 1;
// return println!("{}", err_msg);
// };
println!("{} => Passed", path.display());
// TODO: Remove this later
let mut image = raw_image.process_8bit();
store_image(path, "rawkit", &mut image.data, image.width, image.height);
let processor = Processor::new();
let libraw_image = processor.process_8bit(&content).unwrap();
let mut data = Vec::from_iter(libraw_image.iter().copied());
store_image(path, "libraw_rs", &mut data[..], libraw_image.width() as usize, libraw_image.height() as usize);
true
}
fn store_image(path: &Path, suffix: &str, data: &mut [u8], width: usize, height: usize) {
let mut output_path = PathBuf::new();
if let Some(parent) = path.parent() {
output_path.push(parent);
}
output_path.push("output");
if metadata(&output_path).is_err() {
create_dir(&output_path).unwrap();
}
if let Some(filename) = path.file_stem() {
let new_filename = format!("{}_{}.{}", filename.to_string_lossy(), suffix, "png");
output_path.push(new_filename);
}
output_path.set_extension("png");
let file = BufWriter::new(File::create(output_path).unwrap());
let png_encoder = PngEncoder::new_with_quality(file, CompressionType::Fast, FilterType::Adaptive);
png_encoder.write_image(data, width as u32, height as u32, ColorType::Rgb8.into()).unwrap();
}
fn download_images() {
let mut path = Path::new(BASE_PATH).to_owned();
let client = reqwest::blocking::Client::builder().timeout(Duration::from_secs(60 * 5)).build().unwrap();
for filename in TEST_FILES {
path.push(filename);
if !path.exists() {
let url = BASE_URL.to_owned() + filename;
let mut response = client.get(url).send().unwrap();
let mut file = File::create(BASE_PATH.to_owned() + filename).unwrap();
std::io::copy(&mut response, &mut file).unwrap();
}
path.pop();
}
}
fn test_raw_data(content: &[u8]) -> Result<RawImage, String> {
let processor = libraw::Processor::new();
let libraw_raw_image = processor.decode(content).unwrap();
let mut content = Cursor::new(content);
let raw_image = RawImage::decode(&mut content).unwrap();
if libraw_raw_image.sizes().raw_height as usize != raw_image.height {
return Err(format!(
"The height of raw image is {} but the expected value was {}",
raw_image.height,
libraw_raw_image.sizes().raw_height
));
}
if libraw_raw_image.sizes().raw_width as usize != raw_image.width {
return Err(format!(
"The width of raw image is {} but the expected value was {}",
raw_image.width,
libraw_raw_image.sizes().raw_width
));
}
if (*libraw_raw_image).len() != raw_image.data.len() {
return Err(format!(
"The size of data of raw image is {} but the expected value was {}",
raw_image.data.len(),
(*libraw_raw_image).len()
));
}
if (*libraw_raw_image) != raw_image.data {
let mut err_msg = String::new();
write!(&mut err_msg, "The raw data does not match").unwrap();
if std::env::var("RAWKIT_TEST_PRINT_HISTOGRAM").is_ok() {
writeln!(err_msg).unwrap();
let mut histogram: HashMap<i32, usize> = HashMap::new();
let mut non_zero_count: usize = 0;
(*libraw_raw_image)
.iter()
.zip(raw_image.data.iter())
.map(|(&a, &b)| {
let a: i32 = a.into();
let b: i32 = b.into();
a - b
})
.filter(|&x| x != 0)
.for_each(|x| {
*histogram.entry(x).or_default() += 1;
non_zero_count += 1;
});
let total_pixels = raw_image.height * raw_image.width;
writeln!(err_msg, "{} ({:.5}%) pixels are different from expected", non_zero_count, non_zero_count as f64 / total_pixels as f64).unwrap();
writeln!(err_msg, "Diff Histogram:").unwrap();
let mut items: Vec<_> = histogram.iter().map(|(&a, &b)| (a, b)).collect();
items.sort();
for (key, value) in items {
writeln!(err_msg, "{:05}: {:05} ({:02.5}%)", key, value, value as f64 / total_pixels as f64).unwrap();
}
}
return Err(err_msg);
}
Ok(raw_image)
}
fn _test_final_image(content: &[u8], raw_image: RawImage) -> Result<(), String> {
let processor = libraw::Processor::new();
let libraw_image = processor.process_8bit(content).unwrap();
let image = raw_image.process_8bit();
if libraw_image.height() as usize != image.height {
return Err(format!("The height of image is {} but the expected value was {}", image.height, libraw_image.height()));
}
if libraw_image.width() as usize != image.width {
return Err(format!("The width of image is {} but the expected value was {}", image.width, libraw_image.width()));
}
if (*libraw_image).len() != image.data.len() {
return Err(format!("The size of data of image is {} but the expected value was {}", image.data.len(), (*libraw_image).len()));
}
if (*libraw_image) != image.data {
let mut err_msg = String::new();
write!(&mut err_msg, "The final image does not match").unwrap();
if std::env::var("RAWKIT_TEST_PRINT_HISTOGRAM").is_ok() {
writeln!(err_msg).unwrap();
let mut histogram_red: HashMap<i16, usize> = HashMap::new();
let mut histogram_green: HashMap<i16, usize> = HashMap::new();
let mut histogram_blue: HashMap<i16, usize> = HashMap::new();
let mut non_zero_count: usize = 0;
let mut non_zero_count_red: usize = 0;
let mut non_zero_count_green: usize = 0;
let mut non_zero_count_blue: usize = 0;
(*libraw_image)
.chunks_exact(3)
.zip(image.data.chunks_exact(3))
.map(|(a, b)| {
let a: [u8; 3] = a.try_into().unwrap();
let b: [u8; 3] = b.try_into().unwrap();
(a, b)
})
.map(|([r1, g1, b1], [r2, g2, b2])| {
let r1: i16 = r1.into();
let g1: i16 = g1.into();
let b1: i16 = b1.into();
let r2: i16 = r2.into();
let g2: i16 = g2.into();
let b2: i16 = b2.into();
[r1 - r2, g1 - g2, b1 - b2]
})
.filter(|&[r, g, b]| r != 0 || g != 0 || b != 0)
.for_each(|[r, g, b]| {
non_zero_count += 1;
if r != 0 {
*histogram_red.entry(r).or_default() += 1;
non_zero_count_red += 1;
}
if g != 0 {
*histogram_green.entry(g).or_default() += 1;
non_zero_count_green += 1;
}
if b != 0 {
*histogram_blue.entry(b).or_default() += 1;
non_zero_count_blue += 1;
}
});
let total_pixels = image.height * image.width;
writeln!(err_msg, "{} ({:.5}%) pixels are different from expected", non_zero_count, non_zero_count as f64 / total_pixels as f64,).unwrap();
writeln!(
err_msg,
"{} ({:.5}%) red pixels are different from expected",
non_zero_count_red,
non_zero_count_red as f64 / total_pixels as f64,
)
.unwrap();
writeln!(
err_msg,
"{} ({:.5}%) green pixels are different from expected",
non_zero_count_green,
non_zero_count_green as f64 / total_pixels as f64,
)
.unwrap();
writeln!(
err_msg,
"{} ({:.5}%) blue pixels are different from expected",
non_zero_count_blue,
non_zero_count_blue as f64 / total_pixels as f64,
)
.unwrap();
writeln!(err_msg, "Diff Histogram for Red pixels:").unwrap();
let mut items: Vec<_> = histogram_red.iter().map(|(&a, &b)| (a, b)).collect();
items.sort();
for (key, value) in items {
writeln!(err_msg, "{:05}: {:05} ({:02.5}%)", key, value, value as f64 / total_pixels as f64).unwrap();
}
writeln!(err_msg, "Diff Histogram for Green pixels:").unwrap();
let mut items: Vec<_> = histogram_green.iter().map(|(&a, &b)| (a, b)).collect();
items.sort();
for (key, value) in items {
writeln!(err_msg, "{:05}: {:05} ({:02.5}%)", key, value, value as f64 / total_pixels as f64).unwrap();
}
writeln!(err_msg, "Diff Histogram for Blue pixels:").unwrap();
let mut items: Vec<_> = histogram_blue.iter().map(|(&a, &b)| (a, b)).collect();
items.sort();
for (key, value) in items {
writeln!(err_msg, "{:05}: {:05} ({:02.5}%)", key, value, value as f64 / total_pixels as f64).unwrap();
}
}
return Err(err_msg);
}
Ok(())
}
#[ignore]
#[test]
fn extract_data_from_dng_images() {
read_dir(BASE_PATH)
.unwrap()
.map(|dir_entry| dir_entry.unwrap().path())
.filter(|path| path.is_file() && path.file_name().map(|file_name| file_name != ".gitkeep").unwrap_or(false))
.for_each(|path| {
extract_data_from_dng_image(&path);
});
}
fn extract_data_from_dng_image(path: &Path) {
use rawkit::tiff::Ifd;
use rawkit::tiff::file::TiffRead;
use rawkit::tiff::tags::{ColorMatrix2, Make, Model};
use rawkit::tiff::values::ToFloat;
use std::io::{BufReader, Write};
let reader = BufReader::new(File::open(path).unwrap());
let mut file = TiffRead::new(reader).unwrap();
let ifd = Ifd::new_first_ifd(&mut file).unwrap();
let make = ifd.get_value::<Make, _>(&mut file).unwrap();
let model = ifd.get_value::<Model, _>(&mut file).unwrap();
let matrix = ifd.get_value::<ColorMatrix2, _>(&mut file).unwrap();
if model == "MODEL-NAME" {
println!("{}", path.display());
return;
}
let output_folder = path.parent().unwrap().join(make);
std::fs::create_dir_all(&output_folder).unwrap();
let mut output_file = File::create(output_folder.join(model + ".toml")).unwrap();
let matrix: Vec<_> = matrix.iter().map(|x| x.to_float()).collect();
writeln!(output_file, "camera_to_xyz = {:.4?}", matrix).unwrap();
}
@@ -0,0 +1,10 @@
[package]
name = "wgpu-sync"
description = "Helper for working with wgpu in a multi-threaded context"
version.workspace = true
edition.workspace = true
authors.workspace = true
license.workspace = true
[dependencies]
wgpu = { workspace = true }
@@ -0,0 +1,206 @@
//! Wraps wgpu types to provide synchronization against surface configuration.
//! Everything sharing a [`Instance`] is synchronized against that instance's [`Surface`]s.
//! [`Surface::configure`] takes a write lock, and all other operations take a read lock.
//!
//! [`wgpu::Surface::configure`] recreates the swapchain and waits for the GPU to idle.
//! A concurrent `submit`, `get_current_texture`, or `present` makes that
//! wait fail (validation error, panic, or driver crash on the unsafe hal usage).
//!
//! [`Instance`] and [`Adapter`] wrapper types can be dereferenced to the underlying wgpu type.
//! Their `create_surface`/`request_adapter`/`request_device` methods shadow the wgpu ones.
//! These methods return wrapper types that synchronize against the parent [`Instance`].
//! Be aware that using the underlying wgpu versions directly (through deref) results in unsynchronized objects.
//!
//! Guards hold their read lock for their whole lifetime.
//! While holding a [`SurfaceTextureGuard`] reuse its [`QueueGuard`] via [`SurfaceTextureGuard::queue`] to avoid deadlock.
use std::ops::Deref;
use std::sync::{Arc, PoisonError, RwLock, RwLockReadGuard, RwLockWriteGuard};
#[derive(Clone, Debug)]
struct Lock(Arc<RwLock<()>>);
impl Lock {
fn new() -> Self {
Self(Arc::new(RwLock::new(())))
}
fn read(&self) -> RwLockReadGuard<'_, ()> {
self.0.read().unwrap_or_else(PoisonError::into_inner)
}
fn write(&self) -> RwLockWriteGuard<'_, ()> {
self.0.write().unwrap_or_else(PoisonError::into_inner)
}
}
#[derive(Clone, Debug)]
pub struct Instance {
raw: wgpu::Instance,
lock: Lock,
}
impl Instance {
pub fn new(raw: wgpu::Instance) -> Self {
Self { raw, lock: Lock::new() }
}
pub fn create_surface(&self, target: impl Into<wgpu::SurfaceTarget<'static>>) -> Result<Surface, wgpu::CreateSurfaceError> {
Ok(Surface {
raw: self.raw.create_surface(target)?,
lock: self.lock.clone(),
})
}
pub async fn request_adapter(&self, options: &wgpu::RequestAdapterOptions<'_, '_>) -> Result<Adapter, wgpu::RequestAdapterError> {
Ok(Adapter {
raw: self.raw.request_adapter(options).await?,
lock: self.lock.clone(),
})
}
pub async fn enumerate_adapters(&self, backends: wgpu::Backends) -> Vec<Adapter> {
self.raw
.enumerate_adapters(backends)
.await
.into_iter()
.map(|adapter| Adapter {
raw: adapter,
lock: self.lock.clone(),
})
.collect()
}
}
impl Deref for Instance {
type Target = wgpu::Instance;
fn deref(&self) -> &wgpu::Instance {
&self.raw
}
}
#[derive(Clone, Debug)]
pub struct Adapter {
raw: wgpu::Adapter,
lock: Lock,
}
impl Adapter {
pub async fn request_device(&self, desc: &wgpu::DeviceDescriptor<'_>) -> Result<(wgpu::Device, Queue), wgpu::RequestDeviceError> {
let (device, queue) = self.raw.request_device(desc).await?;
Ok((device, Queue { raw: queue, lock: self.lock.clone() }))
}
}
impl Deref for Adapter {
type Target = wgpu::Adapter;
fn deref(&self) -> &wgpu::Adapter {
&self.raw
}
}
#[derive(Clone, Debug)]
pub struct Queue {
raw: wgpu::Queue,
lock: Lock,
}
impl Queue {
pub fn submit<I: IntoIterator<Item = wgpu::CommandBuffer>>(&self, command_buffers: I) -> wgpu::SubmissionIndex {
self.lock().submit(command_buffers)
}
pub fn lock(&self) -> QueueGuard<'_> {
QueueGuard {
raw: &self.raw,
_guard: self.lock.read(),
}
}
pub fn write_buffer(&self, buffer: &wgpu::Buffer, offset: wgpu::BufferAddress, data: &[u8]) {
self.lock().write_buffer(buffer, offset, data);
}
pub fn write_texture(&self, texture: wgpu::TexelCopyTextureInfo<'_>, data: &[u8], data_layout: wgpu::TexelCopyBufferLayout, size: wgpu::Extent3d) {
self.lock().write_texture(texture, data, data_layout, size);
}
}
pub struct QueueGuard<'a> {
raw: &'a wgpu::Queue,
_guard: RwLockReadGuard<'a, ()>,
}
impl Deref for QueueGuard<'_> {
type Target = wgpu::Queue;
fn deref(&self) -> &wgpu::Queue {
self.raw
}
}
#[derive(Debug)]
pub struct Surface {
raw: wgpu::Surface<'static>,
lock: Lock,
}
impl Surface {
pub fn configure(&self, device: &wgpu::Device, config: &wgpu::SurfaceConfiguration) {
let _guard = self.lock.write();
self.raw.configure(device, config);
}
pub fn get_current_texture<'a>(&self, queue: &'a Queue) -> CurrentSurfaceTexture<'a> {
debug_assert!(Arc::ptr_eq(&self.lock.0, &queue.lock.0), "queue must come from the same `Instance` as this surface");
let guard = queue.lock();
let raw = self.raw.get_current_texture();
match raw {
wgpu::CurrentSurfaceTexture::Success(raw) => CurrentSurfaceTexture::Success(SurfaceTextureGuard { raw, queue: guard }),
wgpu::CurrentSurfaceTexture::Suboptimal(raw) => CurrentSurfaceTexture::Suboptimal(SurfaceTextureGuard { raw, queue: guard }),
wgpu::CurrentSurfaceTexture::Occluded => CurrentSurfaceTexture::Occluded,
wgpu::CurrentSurfaceTexture::Lost => CurrentSurfaceTexture::Lost,
wgpu::CurrentSurfaceTexture::Outdated => CurrentSurfaceTexture::Outdated,
wgpu::CurrentSurfaceTexture::Timeout => CurrentSurfaceTexture::Timeout,
wgpu::CurrentSurfaceTexture::Validation => CurrentSurfaceTexture::Validation,
}
}
pub fn get_capabilities(&self, adapter: &wgpu::Adapter) -> wgpu::SurfaceCapabilities {
self.raw.get_capabilities(adapter)
}
}
#[derive(Debug)]
pub enum CurrentSurfaceTexture<'a> {
Success(SurfaceTextureGuard<'a>),
Suboptimal(SurfaceTextureGuard<'a>),
Occluded,
Lost,
Outdated,
Timeout,
Validation,
}
pub struct SurfaceTextureGuard<'a> {
raw: wgpu::SurfaceTexture,
pub queue: QueueGuard<'a>,
}
impl SurfaceTextureGuard<'_> {
pub fn present(self) {
self.raw.present();
}
}
impl Deref for SurfaceTextureGuard<'_> {
type Target = wgpu::SurfaceTexture;
fn deref(&self) -> &Self::Target {
&self.raw
}
}
impl std::fmt::Debug for SurfaceTextureGuard<'_> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("SurfaceTexture").field("raw", &self.raw).finish()
}
}