]> patthoyts.tk Git - srfdump/commitdiff
C# implementation of JPEG and EXIF reader
authorPat Thoyts <pat.thoyts@gmail.com>
Sun, 14 Dec 2025 22:26:01 +0000 (22:26 +0000)
committerPat Thoyts <pat.thoyts@gmail.com>
Sun, 14 Dec 2025 22:26:01 +0000 (22:26 +0000)
dotnet/.editorconfig [new file with mode: 0644]
dotnet/.gitignore [new file with mode: 0644]
dotnet/JpegUtil.Test/Data/a.jpg [new file with mode: 0644]
dotnet/JpegUtil.Test/JpegReaderTests.cs [new file with mode: 0644]
dotnet/JpegUtil.Test/JpegUtil.Test.csproj [new file with mode: 0644]
dotnet/JpegUtil.sln [new file with mode: 0644]
dotnet/JpegUtil/Exif.cs [new file with mode: 0644]
dotnet/JpegUtil/JpegReader.cs [new file with mode: 0644]
dotnet/JpegUtil/JpegUtil.csproj [new file with mode: 0644]

diff --git a/dotnet/.editorconfig b/dotnet/.editorconfig
new file mode 100644 (file)
index 0000000..668ff28
--- /dev/null
@@ -0,0 +1,378 @@
+root = true
+
+# All files
+[*]
+indent_style = space
+
+# Xml files
+[*.xml]
+indent_size = 2
+
+# C# files
+[*.cs]
+
+#### Core EditorConfig Options ####
+
+# Indentation and spacing
+indent_size = 4
+tab_width = 4
+
+# New line preferences
+insert_final_newline = false
+
+#### .NET Coding Conventions ####
+[*.{cs,vb}]
+
+# Organize usings
+dotnet_separate_import_directive_groups = true
+dotnet_sort_system_directives_first = true
+file_header_template = unset
+
+# this. and Me. preferences
+dotnet_style_qualification_for_event = false:silent
+dotnet_style_qualification_for_field = false:silent
+dotnet_style_qualification_for_method = false:silent
+dotnet_style_qualification_for_property = false:silent
+
+# Language keywords vs BCL types preferences
+dotnet_style_predefined_type_for_locals_parameters_members = true:silent
+dotnet_style_predefined_type_for_member_access = true:silent
+
+# Parentheses preferences
+dotnet_style_parentheses_in_arithmetic_binary_operators = always_for_clarity:silent
+dotnet_style_parentheses_in_other_binary_operators = always_for_clarity:silent
+dotnet_style_parentheses_in_other_operators = never_if_unnecessary:silent
+dotnet_style_parentheses_in_relational_binary_operators = always_for_clarity:silent
+
+# Modifier preferences
+dotnet_style_require_accessibility_modifiers = for_non_interface_members:silent
+
+# Expression-level preferences
+dotnet_style_coalesce_expression = true:suggestion
+dotnet_style_collection_initializer = true:suggestion
+dotnet_style_explicit_tuple_names = true:suggestion
+dotnet_style_namespace_match_folder = true:suggestion
+dotnet_style_null_propagation = true:suggestion
+dotnet_style_object_initializer = true:suggestion
+dotnet_style_operator_placement_when_wrapping = beginning_of_line
+dotnet_style_prefer_auto_properties = true:suggestion
+dotnet_style_prefer_collection_expression = when_types_loosely_match:suggestion
+dotnet_style_prefer_compound_assignment = true:suggestion
+dotnet_style_prefer_conditional_expression_over_assignment = true:suggestion
+dotnet_style_prefer_conditional_expression_over_return = true:suggestion
+dotnet_style_prefer_foreach_explicit_cast_in_source = when_strongly_typed:suggestion
+dotnet_style_prefer_inferred_anonymous_type_member_names = true:suggestion
+dotnet_style_prefer_inferred_tuple_names = true:suggestion
+dotnet_style_prefer_is_null_check_over_reference_equality_method = true:suggestion
+dotnet_style_prefer_simplified_boolean_expressions = true:suggestion
+dotnet_style_prefer_simplified_interpolation = true:suggestion
+
+# Field preferences
+dotnet_style_readonly_field = true:warning
+
+# Parameter preferences
+dotnet_code_quality_unused_parameters = all:suggestion
+
+# Suppression preferences
+dotnet_remove_unnecessary_suppression_exclusions = none
+
+#### C# Coding Conventions ####
+[*.cs]
+
+# var preferences
+csharp_style_var_elsewhere = false:silent
+csharp_style_var_for_built_in_types = false:silent
+csharp_style_var_when_type_is_apparent = false:silent
+
+# Expression-bodied members
+csharp_style_expression_bodied_accessors = true:silent
+csharp_style_expression_bodied_constructors = false:silent
+csharp_style_expression_bodied_indexers = true:silent
+csharp_style_expression_bodied_lambdas = true:suggestion
+csharp_style_expression_bodied_local_functions = false:silent
+csharp_style_expression_bodied_methods = false:silent
+csharp_style_expression_bodied_operators = false:silent
+csharp_style_expression_bodied_properties = true:silent
+
+# Pattern matching preferences
+csharp_style_pattern_matching_over_as_with_null_check = true:suggestion
+csharp_style_pattern_matching_over_is_with_cast_check = true:suggestion
+csharp_style_prefer_extended_property_pattern = true:suggestion
+csharp_style_prefer_not_pattern = true:suggestion
+csharp_style_prefer_pattern_matching = true:silent
+csharp_style_prefer_switch_expression = true:suggestion
+
+# Null-checking preferences
+csharp_style_conditional_delegate_call = true:suggestion
+
+# Modifier preferences
+csharp_prefer_static_anonymous_function = true:suggestion
+csharp_prefer_static_local_function = true:warning
+csharp_preferred_modifier_order = public,private,protected,internal,file,const,static,extern,new,virtual,abstract,sealed,override,readonly,unsafe,required,volatile,async:suggestion
+csharp_style_prefer_readonly_struct = true:suggestion
+csharp_style_prefer_readonly_struct_member = true:suggestion
+
+# Code-block preferences
+csharp_prefer_braces = true:silent
+csharp_prefer_simple_using_statement = true:suggestion
+csharp_style_namespace_declarations = file_scoped:suggestion
+csharp_style_prefer_method_group_conversion = true:silent
+csharp_style_prefer_primary_constructors = true:suggestion
+csharp_style_prefer_top_level_statements = true:silent
+
+# Expression-level preferences
+csharp_prefer_simple_default_expression = true:suggestion
+csharp_style_deconstructed_variable_declaration = true:suggestion
+csharp_style_implicit_object_creation_when_type_is_apparent = true:suggestion
+csharp_style_inlined_variable_declaration = true:suggestion
+csharp_style_prefer_index_operator = true:suggestion
+csharp_style_prefer_local_over_anonymous_function = true:suggestion
+csharp_style_prefer_null_check_over_type_check = true:suggestion
+csharp_style_prefer_range_operator = true:suggestion
+csharp_style_prefer_tuple_swap = true:suggestion
+csharp_style_prefer_utf8_string_literals = true:suggestion
+csharp_style_throw_expression = true:suggestion
+csharp_style_unused_value_assignment_preference = discard_variable:suggestion
+csharp_style_unused_value_expression_statement_preference = discard_variable:silent
+
+# 'using' directive preferences
+csharp_using_directive_placement = outside_namespace:silent
+
+#### C# Formatting Rules ####
+
+# New line preferences
+csharp_new_line_before_catch = true
+csharp_new_line_before_else = true
+csharp_new_line_before_finally = true
+csharp_new_line_before_members_in_anonymous_types = true
+csharp_new_line_before_members_in_object_initializers = true
+csharp_new_line_before_open_brace = all
+csharp_new_line_between_query_expression_clauses = true
+
+# Indentation preferences
+csharp_indent_block_contents = true
+csharp_indent_braces = false
+csharp_indent_case_contents = true
+csharp_indent_case_contents_when_block = false
+csharp_indent_labels = one_less_than_current
+csharp_indent_switch_labels = true
+
+# Space preferences
+csharp_space_after_cast = false
+csharp_space_after_colon_in_inheritance_clause = true
+csharp_space_after_comma = true
+csharp_space_after_dot = false
+csharp_space_after_keywords_in_control_flow_statements = true
+csharp_space_after_semicolon_in_for_statement = true
+csharp_space_around_binary_operators = before_and_after
+csharp_space_around_declaration_statements = false
+csharp_space_before_colon_in_inheritance_clause = true
+csharp_space_before_comma = false
+csharp_space_before_dot = false
+csharp_space_before_open_square_brackets = false
+csharp_space_before_semicolon_in_for_statement = false
+csharp_space_between_empty_square_brackets = false
+csharp_space_between_method_call_empty_parameter_list_parentheses = false
+csharp_space_between_method_call_name_and_opening_parenthesis = false
+csharp_space_between_method_call_parameter_list_parentheses = false
+csharp_space_between_method_declaration_empty_parameter_list_parentheses = false
+csharp_space_between_method_declaration_name_and_open_parenthesis = false
+csharp_space_between_method_declaration_parameter_list_parentheses = false
+csharp_space_between_parentheses = false
+csharp_space_between_square_brackets = false
+
+# Wrapping preferences
+csharp_preserve_single_line_blocks = true
+csharp_preserve_single_line_statements = true
+
+#### Naming styles ####
+[*.{cs,vb}]
+
+# Naming rules
+
+dotnet_naming_rule.types_and_namespaces_should_be_pascalcase.severity = suggestion
+dotnet_naming_rule.types_and_namespaces_should_be_pascalcase.symbols = types_and_namespaces
+dotnet_naming_rule.types_and_namespaces_should_be_pascalcase.style = pascalcase
+
+dotnet_naming_rule.interfaces_should_be_ipascalcase.severity = suggestion
+dotnet_naming_rule.interfaces_should_be_ipascalcase.symbols = interfaces
+dotnet_naming_rule.interfaces_should_be_ipascalcase.style = ipascalcase
+
+dotnet_naming_rule.type_parameters_should_be_tpascalcase.severity = suggestion
+dotnet_naming_rule.type_parameters_should_be_tpascalcase.symbols = type_parameters
+dotnet_naming_rule.type_parameters_should_be_tpascalcase.style = tpascalcase
+
+dotnet_naming_rule.methods_should_be_pascalcase.severity = suggestion
+dotnet_naming_rule.methods_should_be_pascalcase.symbols = methods
+dotnet_naming_rule.methods_should_be_pascalcase.style = pascalcase
+
+dotnet_naming_rule.properties_should_be_pascalcase.severity = suggestion
+dotnet_naming_rule.properties_should_be_pascalcase.symbols = properties
+dotnet_naming_rule.properties_should_be_pascalcase.style = pascalcase
+
+dotnet_naming_rule.events_should_be_pascalcase.severity = suggestion
+dotnet_naming_rule.events_should_be_pascalcase.symbols = events
+dotnet_naming_rule.events_should_be_pascalcase.style = pascalcase
+
+dotnet_naming_rule.local_variables_should_be_camelcase.severity = suggestion
+dotnet_naming_rule.local_variables_should_be_camelcase.symbols = local_variables
+dotnet_naming_rule.local_variables_should_be_camelcase.style = camelcase
+
+dotnet_naming_rule.local_constants_should_be_camelcase.severity = suggestion
+dotnet_naming_rule.local_constants_should_be_camelcase.symbols = local_constants
+dotnet_naming_rule.local_constants_should_be_camelcase.style = camelcase
+
+dotnet_naming_rule.parameters_should_be_camelcase.severity = suggestion
+dotnet_naming_rule.parameters_should_be_camelcase.symbols = parameters
+dotnet_naming_rule.parameters_should_be_camelcase.style = camelcase
+
+dotnet_naming_rule.public_fields_should_be_pascalcase.severity = suggestion
+dotnet_naming_rule.public_fields_should_be_pascalcase.symbols = public_fields
+dotnet_naming_rule.public_fields_should_be_pascalcase.style = pascalcase
+
+dotnet_naming_rule.private_fields_should_be__camelcase.severity = suggestion
+dotnet_naming_rule.private_fields_should_be__camelcase.symbols = private_fields
+dotnet_naming_rule.private_fields_should_be__camelcase.style = _camelcase
+
+dotnet_naming_rule.private_static_fields_should_be_s_camelcase.severity = suggestion
+dotnet_naming_rule.private_static_fields_should_be_s_camelcase.symbols = private_static_fields
+dotnet_naming_rule.private_static_fields_should_be_s_camelcase.style = s_camelcase
+
+dotnet_naming_rule.public_constant_fields_should_be_pascalcase.severity = suggestion
+dotnet_naming_rule.public_constant_fields_should_be_pascalcase.symbols = public_constant_fields
+dotnet_naming_rule.public_constant_fields_should_be_pascalcase.style = pascalcase
+
+dotnet_naming_rule.private_constant_fields_should_be_pascalcase.severity = suggestion
+dotnet_naming_rule.private_constant_fields_should_be_pascalcase.symbols = private_constant_fields
+dotnet_naming_rule.private_constant_fields_should_be_pascalcase.style = pascalcase
+
+dotnet_naming_rule.public_static_readonly_fields_should_be_pascalcase.severity = suggestion
+dotnet_naming_rule.public_static_readonly_fields_should_be_pascalcase.symbols = public_static_readonly_fields
+dotnet_naming_rule.public_static_readonly_fields_should_be_pascalcase.style = pascalcase
+
+dotnet_naming_rule.private_static_readonly_fields_should_be_pascalcase.severity = suggestion
+dotnet_naming_rule.private_static_readonly_fields_should_be_pascalcase.symbols = private_static_readonly_fields
+dotnet_naming_rule.private_static_readonly_fields_should_be_pascalcase.style = pascalcase
+
+dotnet_naming_rule.enums_should_be_pascalcase.severity = suggestion
+dotnet_naming_rule.enums_should_be_pascalcase.symbols = enums
+dotnet_naming_rule.enums_should_be_pascalcase.style = pascalcase
+
+dotnet_naming_rule.local_functions_should_be_pascalcase.severity = suggestion
+dotnet_naming_rule.local_functions_should_be_pascalcase.symbols = local_functions
+dotnet_naming_rule.local_functions_should_be_pascalcase.style = pascalcase
+
+dotnet_naming_rule.non_field_members_should_be_pascalcase.severity = suggestion
+dotnet_naming_rule.non_field_members_should_be_pascalcase.symbols = non_field_members
+dotnet_naming_rule.non_field_members_should_be_pascalcase.style = pascalcase
+
+# Symbol specifications
+
+dotnet_naming_symbols.interfaces.applicable_kinds = interface
+dotnet_naming_symbols.interfaces.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected
+dotnet_naming_symbols.interfaces.required_modifiers = 
+
+dotnet_naming_symbols.enums.applicable_kinds = enum
+dotnet_naming_symbols.enums.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected
+dotnet_naming_symbols.enums.required_modifiers = 
+
+dotnet_naming_symbols.events.applicable_kinds = event
+dotnet_naming_symbols.events.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected
+dotnet_naming_symbols.events.required_modifiers = 
+
+dotnet_naming_symbols.methods.applicable_kinds = method
+dotnet_naming_symbols.methods.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected
+dotnet_naming_symbols.methods.required_modifiers = 
+
+dotnet_naming_symbols.properties.applicable_kinds = property
+dotnet_naming_symbols.properties.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected
+dotnet_naming_symbols.properties.required_modifiers = 
+
+dotnet_naming_symbols.public_fields.applicable_kinds = field
+dotnet_naming_symbols.public_fields.applicable_accessibilities = public, internal
+dotnet_naming_symbols.public_fields.required_modifiers = 
+
+dotnet_naming_symbols.private_fields.applicable_kinds = field
+dotnet_naming_symbols.private_fields.applicable_accessibilities = private, protected, protected_internal, private_protected
+dotnet_naming_symbols.private_fields.required_modifiers = 
+
+dotnet_naming_symbols.private_static_fields.applicable_kinds = field
+dotnet_naming_symbols.private_static_fields.applicable_accessibilities = private, protected, protected_internal, private_protected
+dotnet_naming_symbols.private_static_fields.required_modifiers = static
+
+dotnet_naming_symbols.types_and_namespaces.applicable_kinds = namespace, class, struct, interface, enum
+dotnet_naming_symbols.types_and_namespaces.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected
+dotnet_naming_symbols.types_and_namespaces.required_modifiers = 
+
+dotnet_naming_symbols.non_field_members.applicable_kinds = property, event, method
+dotnet_naming_symbols.non_field_members.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected
+dotnet_naming_symbols.non_field_members.required_modifiers = 
+
+dotnet_naming_symbols.type_parameters.applicable_kinds = namespace
+dotnet_naming_symbols.type_parameters.applicable_accessibilities = *
+dotnet_naming_symbols.type_parameters.required_modifiers = 
+
+dotnet_naming_symbols.private_constant_fields.applicable_kinds = field
+dotnet_naming_symbols.private_constant_fields.applicable_accessibilities = private, protected, protected_internal, private_protected
+dotnet_naming_symbols.private_constant_fields.required_modifiers = const
+
+dotnet_naming_symbols.local_variables.applicable_kinds = local
+dotnet_naming_symbols.local_variables.applicable_accessibilities = local
+dotnet_naming_symbols.local_variables.required_modifiers = 
+
+dotnet_naming_symbols.local_constants.applicable_kinds = local
+dotnet_naming_symbols.local_constants.applicable_accessibilities = local
+dotnet_naming_symbols.local_constants.required_modifiers = const
+
+dotnet_naming_symbols.parameters.applicable_kinds = parameter
+dotnet_naming_symbols.parameters.applicable_accessibilities = *
+dotnet_naming_symbols.parameters.required_modifiers = 
+
+dotnet_naming_symbols.public_constant_fields.applicable_kinds = field
+dotnet_naming_symbols.public_constant_fields.applicable_accessibilities = public, internal
+dotnet_naming_symbols.public_constant_fields.required_modifiers = const
+
+dotnet_naming_symbols.public_static_readonly_fields.applicable_kinds = field
+dotnet_naming_symbols.public_static_readonly_fields.applicable_accessibilities = public, internal
+dotnet_naming_symbols.public_static_readonly_fields.required_modifiers = readonly, static
+
+dotnet_naming_symbols.private_static_readonly_fields.applicable_kinds = field
+dotnet_naming_symbols.private_static_readonly_fields.applicable_accessibilities = private, protected, protected_internal, private_protected
+dotnet_naming_symbols.private_static_readonly_fields.required_modifiers = readonly, static
+
+dotnet_naming_symbols.local_functions.applicable_kinds = local_function
+dotnet_naming_symbols.local_functions.applicable_accessibilities = *
+dotnet_naming_symbols.local_functions.required_modifiers = 
+
+# Naming styles
+
+dotnet_naming_style.pascalcase.required_prefix = 
+dotnet_naming_style.pascalcase.required_suffix = 
+dotnet_naming_style.pascalcase.word_separator = 
+dotnet_naming_style.pascalcase.capitalization = pascal_case
+
+dotnet_naming_style.ipascalcase.required_prefix = I
+dotnet_naming_style.ipascalcase.required_suffix = 
+dotnet_naming_style.ipascalcase.word_separator = 
+dotnet_naming_style.ipascalcase.capitalization = pascal_case
+
+dotnet_naming_style.tpascalcase.required_prefix = T
+dotnet_naming_style.tpascalcase.required_suffix = 
+dotnet_naming_style.tpascalcase.word_separator = 
+dotnet_naming_style.tpascalcase.capitalization = pascal_case
+
+dotnet_naming_style._camelcase.required_prefix = _
+dotnet_naming_style._camelcase.required_suffix = 
+dotnet_naming_style._camelcase.word_separator = 
+dotnet_naming_style._camelcase.capitalization = camel_case
+
+dotnet_naming_style.camelcase.required_prefix = 
+dotnet_naming_style.camelcase.required_suffix = 
+dotnet_naming_style.camelcase.word_separator = 
+dotnet_naming_style.camelcase.capitalization = camel_case
+
+dotnet_naming_style.s_camelcase.required_prefix = s_
+dotnet_naming_style.s_camelcase.required_suffix = 
+dotnet_naming_style.s_camelcase.word_separator = 
+dotnet_naming_style.s_camelcase.capitalization = camel_case
+
diff --git a/dotnet/.gitignore b/dotnet/.gitignore
new file mode 100644 (file)
index 0000000..fab8f5d
--- /dev/null
@@ -0,0 +1,5 @@
+bin/
+obj/
+.vs/
+.vscode
+
diff --git a/dotnet/JpegUtil.Test/Data/a.jpg b/dotnet/JpegUtil.Test/Data/a.jpg
new file mode 100644 (file)
index 0000000..97b7d0e
Binary files /dev/null and b/dotnet/JpegUtil.Test/Data/a.jpg differ
diff --git a/dotnet/JpegUtil.Test/JpegReaderTests.cs b/dotnet/JpegUtil.Test/JpegReaderTests.cs
new file mode 100644 (file)
index 0000000..85d009e
--- /dev/null
@@ -0,0 +1,28 @@
+using Xunit.Abstractions;
+using Shouldly;
+
+namespace JpegUtil.Test;
+
+public class JpegReaderTests
+{
+    const string testPath = "Data/a.jpg";
+
+    private ITestOutputHelper _outputhelper { get; init; }
+    public JpegReaderTests(ITestOutputHelper testOutputHelper)
+    {
+        _outputhelper = testOutputHelper;
+    }
+
+    [Fact]
+    public void Test1()
+    {
+        var path = Path.IsPathRooted(testPath)
+            ? testPath
+            : Path.GetRelativePath(Directory.GetCurrentDirectory(), "../../../" + testPath);
+        using var stream = new FileStream(path, FileMode.Open, FileAccess.Read, FileShare.Read);
+        using var reader = new JpegReader(stream, _outputhelper.WriteLine);
+        var data = reader.Exif().ToList();
+        data.Count.ShouldBe(12);
+        // data.Select(x => x. ShouldContain()
+    }
+}
diff --git a/dotnet/JpegUtil.Test/JpegUtil.Test.csproj b/dotnet/JpegUtil.Test/JpegUtil.Test.csproj
new file mode 100644 (file)
index 0000000..20922d7
--- /dev/null
@@ -0,0 +1,26 @@
+<Project Sdk="Microsoft.NET.Sdk">
+
+  <PropertyGroup>
+    <TargetFramework>net9.0</TargetFramework>
+    <ImplicitUsings>enable</ImplicitUsings>
+    <Nullable>enable</Nullable>
+    <IsPackable>false</IsPackable>
+  </PropertyGroup>
+
+  <ItemGroup>
+    <PackageReference Include="coverlet.collector" Version="6.0.2" />
+    <PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.12.0" />
+    <PackageReference Include="xunit" Version="2.9.3" />
+    <PackageReference Include="xunit.runner.visualstudio" Version="3.1.5" />
+    <PackageReference Include="Shouldly" Version="4.3.0" />
+  </ItemGroup>
+
+  <ItemGroup>
+    <ProjectReference Include="../JpegUtil/JpegUtil.csproj" />
+  </ItemGroup>
+
+  <ItemGroup>
+    <Using Include="Xunit" />
+  </ItemGroup>
+
+</Project>
diff --git a/dotnet/JpegUtil.sln b/dotnet/JpegUtil.sln
new file mode 100644 (file)
index 0000000..fceb358
--- /dev/null
@@ -0,0 +1,48 @@
+
+Microsoft Visual Studio Solution File, Format Version 12.00
+# Visual Studio Version 17
+VisualStudioVersion = 17.0.31903.59
+MinimumVisualStudioVersion = 10.0.40219.1
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "JpegUtil", "JpegUtil\JpegUtil.csproj", "{6380C354-3DC0-4BEF-82D4-69DAE207A908}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "JpegUtil.Test", "JpegUtil.Test\JpegUtil.Test.csproj", "{2CF6525D-DBFB-4B8D-877D-17CAE798D9CA}"
+EndProject
+Global
+       GlobalSection(SolutionConfigurationPlatforms) = preSolution
+               Debug|Any CPU = Debug|Any CPU
+               Debug|x64 = Debug|x64
+               Debug|x86 = Debug|x86
+               Release|Any CPU = Release|Any CPU
+               Release|x64 = Release|x64
+               Release|x86 = Release|x86
+       EndGlobalSection
+       GlobalSection(ProjectConfigurationPlatforms) = postSolution
+               {6380C354-3DC0-4BEF-82D4-69DAE207A908}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+               {6380C354-3DC0-4BEF-82D4-69DAE207A908}.Debug|Any CPU.Build.0 = Debug|Any CPU
+               {6380C354-3DC0-4BEF-82D4-69DAE207A908}.Debug|x64.ActiveCfg = Debug|Any CPU
+               {6380C354-3DC0-4BEF-82D4-69DAE207A908}.Debug|x64.Build.0 = Debug|Any CPU
+               {6380C354-3DC0-4BEF-82D4-69DAE207A908}.Debug|x86.ActiveCfg = Debug|Any CPU
+               {6380C354-3DC0-4BEF-82D4-69DAE207A908}.Debug|x86.Build.0 = Debug|Any CPU
+               {6380C354-3DC0-4BEF-82D4-69DAE207A908}.Release|Any CPU.ActiveCfg = Release|Any CPU
+               {6380C354-3DC0-4BEF-82D4-69DAE207A908}.Release|Any CPU.Build.0 = Release|Any CPU
+               {6380C354-3DC0-4BEF-82D4-69DAE207A908}.Release|x64.ActiveCfg = Release|Any CPU
+               {6380C354-3DC0-4BEF-82D4-69DAE207A908}.Release|x64.Build.0 = Release|Any CPU
+               {6380C354-3DC0-4BEF-82D4-69DAE207A908}.Release|x86.ActiveCfg = Release|Any CPU
+               {6380C354-3DC0-4BEF-82D4-69DAE207A908}.Release|x86.Build.0 = Release|Any CPU
+               {2CF6525D-DBFB-4B8D-877D-17CAE798D9CA}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+               {2CF6525D-DBFB-4B8D-877D-17CAE798D9CA}.Debug|Any CPU.Build.0 = Debug|Any CPU
+               {2CF6525D-DBFB-4B8D-877D-17CAE798D9CA}.Debug|x64.ActiveCfg = Debug|Any CPU
+               {2CF6525D-DBFB-4B8D-877D-17CAE798D9CA}.Debug|x64.Build.0 = Debug|Any CPU
+               {2CF6525D-DBFB-4B8D-877D-17CAE798D9CA}.Debug|x86.ActiveCfg = Debug|Any CPU
+               {2CF6525D-DBFB-4B8D-877D-17CAE798D9CA}.Debug|x86.Build.0 = Debug|Any CPU
+               {2CF6525D-DBFB-4B8D-877D-17CAE798D9CA}.Release|Any CPU.ActiveCfg = Release|Any CPU
+               {2CF6525D-DBFB-4B8D-877D-17CAE798D9CA}.Release|Any CPU.Build.0 = Release|Any CPU
+               {2CF6525D-DBFB-4B8D-877D-17CAE798D9CA}.Release|x64.ActiveCfg = Release|Any CPU
+               {2CF6525D-DBFB-4B8D-877D-17CAE798D9CA}.Release|x64.Build.0 = Release|Any CPU
+               {2CF6525D-DBFB-4B8D-877D-17CAE798D9CA}.Release|x86.ActiveCfg = Release|Any CPU
+               {2CF6525D-DBFB-4B8D-877D-17CAE798D9CA}.Release|x86.Build.0 = Release|Any CPU
+       EndGlobalSection
+       GlobalSection(SolutionProperties) = preSolution
+               HideSolutionNode = FALSE
+       EndGlobalSection
+EndGlobal
diff --git a/dotnet/JpegUtil/Exif.cs b/dotnet/JpegUtil/Exif.cs
new file mode 100644 (file)
index 0000000..46720f0
--- /dev/null
@@ -0,0 +1,295 @@
+using System;
+using System.Collections;
+using System.Collections.Generic;
+using System.Globalization;
+using System.Linq;
+using System.Text;
+
+using ExifTag = ushort;
+
+namespace JpegUtil;
+
+public enum ExifType
+{
+    Byte = 1, // uint8_t
+    Ascii = 2, // ASCII NUL terminated
+    Short = 3, // uint16_t
+    Long = 4, // uint32_t
+    Rational = 5, // 2 LONGs numerator : denominator
+    SByte = 6, // int8_t
+    Undefined = 7, // uint8_t
+    SShort = 8, // int16_t
+    SLong = 9, // int32_t
+    SRational = 10, // 2 SLONGs numerator : denominator
+    Float32 = 11, // IEEE 32 bit floating point
+    Float64 = 12 // IEEE 64 bit floating point
+}
+
+public struct ExifItem
+{
+    public ExifTag Tag { get; private set; }
+    public ExifType Type { get; private set; }
+    public object Value { get; private set; }
+
+    const ExifTag EXIF_IFD_TAG = 0x8769; // special tag that is a pointer to more tags
+    const ushort EXIF_IFD_SIZE = 12; // size in bytes of an IFD entry
+    const ushort EXIF_USHORT_SIZE = 2;
+    const ushort EXIF_BOM_BE = 0x4d4d;
+    const ushort EXIF_BOM_LE = 0x4949;
+    const ushort EXIF_HDR_MAGIC = 0x2a;
+
+    private static bool IsBigEndian = false;
+
+    private static ushort get_uint16(byte[] data, int startIndex)
+    {
+        return IsBigEndian
+            ? (ushort)((data[startIndex] << 8) | data[startIndex + 1])
+            : BitConverter.ToUInt16(data, startIndex);
+    }
+
+    private static uint get_uint32(byte[] data, int startIndex)
+    {
+        return IsBigEndian
+            ? (uint)(
+                  (data[startIndex + 0] << 24)
+                | (data[startIndex + 1] << 16)
+                | (data[startIndex + 2] << 8)
+                | (data[startIndex + 3]))
+            : BitConverter.ToUInt32(data, startIndex);
+    }
+    private static int get_int32(byte[] data, int startIndex)
+    {
+        return IsBigEndian
+            ? (int)(
+                  (data[startIndex + 0] << 24)
+                | (data[startIndex + 1] << 16)
+                | (data[startIndex + 2] << 8)
+                | (data[startIndex + 3]))
+            : BitConverter.ToInt32(data, startIndex);
+    }
+
+    public static IEnumerable<ExifItem> ExifParseItem(byte[] data, int off)
+    {
+        // uint item_count = 0;
+        ushort tag = get_uint16(data, off);
+        ExifType type = (ExifType)get_uint16(data, off + 2);
+        int count = (int)get_uint32(data, off + 4);
+        int dataOffset = (int)get_uint32(data, off + 8);
+        int hdrEnd = off + 8;
+
+        object? value = null;
+        switch (type)
+        {
+            case ExifType.Byte:
+            case ExifType.Undefined:
+                if (count < 5)
+                    dataOffset = hdrEnd;
+                value = new ArraySegment<byte>(data, dataOffset, count);
+                break;
+            case ExifType.Ascii:
+            {
+                if (count < 5)
+                    dataOffset = hdrEnd;
+                value = Encoding.ASCII.GetString(data, dataOffset, count);
+                break;
+            }
+            case ExifType.Short:
+            {
+                var lst = new List<ushort>();
+                if (count < 3) // handle small values being stored in the offset area
+                    dataOffset = hdrEnd;
+                for (int n = 0; n < count; ++n)
+                {
+                    lst.Add(get_uint16(data, dataOffset + (n * 2)));
+                    dataOffset += 2;
+                }
+                value = count == 1 ? lst[0] : lst;
+                break;
+            }
+            case ExifType.Long:
+            {
+                if (tag == EXIF_IFD_TAG)
+                {
+                    ushort ifd_count = get_uint16(data, dataOffset);
+                    dataOffset += 2;
+                    for (int n = 0; n < ifd_count; ++n)
+                    {
+                        foreach (var item in ExifParseItem(data, dataOffset + (n * EXIF_IFD_SIZE)))
+                        {
+                            yield return item;
+                        }
+                    }
+                }
+                else
+                {
+                    var lst = new List<uint>();
+                    if (count == 1)
+                        dataOffset = hdrEnd;
+                    for (int n = 0; n < count; ++n)
+                    {
+                        lst.Add(get_uint32(data, dataOffset + (n * 4)));
+                        dataOffset += 4;
+                    }
+                    value = count == 1 ? lst[0] : lst;
+                }
+                break;
+            }
+            case ExifType.Rational:
+            {
+                var lst = new List<Tuple<uint, uint>>();
+                for (int n = 0; n < count; ++n)
+                {
+                    lst.Add(new Tuple<uint, uint>(
+                        get_uint32(data, dataOffset),
+                        get_uint32(data, dataOffset + 4)
+                    ));
+                    dataOffset += 8;
+                }
+                value = count == 1 ? lst[0] : lst;
+                break;
+            }
+            case ExifType.SLong:
+            {
+                var lst = new List<int>();
+                if (count == 1)
+                    dataOffset = hdrEnd;
+                for (int n = 0; n < count; ++n)
+                {
+                    lst.Add(get_int32(data, dataOffset + (n * 4)));
+                    dataOffset += 4;
+                }
+                value = count == 1 ? lst[0] : lst;
+                break;
+            }
+            case ExifType.SRational:
+            {
+                var lst = new List<Tuple<int, int>>();
+                for (int n = 0; n < count; ++n)
+                {
+                    lst.Add(new Tuple<int, int>(
+                        get_int32(data, dataOffset),
+                        get_int32(data, dataOffset + 4)
+                    ));
+                    dataOffset += 8;
+                }
+                value = count == 1 ? lst[0] : lst;
+                break;
+            }
+            default:
+                throw new Exception($"unhandled EXIF type {Enum.GetName(typeof(ExifType), type)}");
+        }
+
+        if (value != null)
+        {
+            yield return new ExifItem()
+            {
+                Tag = tag,
+                Type = type,
+                Value = value
+            };
+        }
+    }
+
+    public static IEnumerable<ExifItem> ExifParse(byte[] data)
+    {
+        List<ExifItem> items = new();
+        IsBigEndian = get_uint16(data, 0) == EXIF_BOM_BE;
+        ushort magic = get_uint16(data, 2);
+        if (magic != EXIF_HDR_MAGIC)
+            throw new Exception("invalid exif magic");
+        uint offset = get_uint32(data, 4);
+        while (offset != 0)
+        {
+            // read the number of EXIF items stored (big endian)
+            ushort count = get_uint16(data, (int)offset);
+
+            // process each EXIF tag from the buffer
+            int entry = (int)offset + 2;
+            for (ushort n = 0; n < count; ++n, entry += EXIF_IFD_SIZE)
+            {
+                foreach (var item in ExifParseItem(data, entry))
+                {
+                    Console.WriteLine(item.Print());
+                    yield return item;
+                }
+            }
+            // after the IFD table is an offset to the next IFD table (if any)
+            offset = get_uint16(data, (int)(offset + EXIF_USHORT_SIZE + (count * EXIF_IFD_SIZE)));
+        }
+    }
+}
+
+public static class ObjectExtensions
+{
+    public static bool IsGenericList(this object o)
+    {
+        var oType = o.GetType();
+        return (oType.IsGenericType && (oType.GetGenericTypeDefinition() == typeof(List<>)));
+    }
+}
+
+public static class ExifExtensions
+{
+    public static string Print(this ExifItem item)
+    {
+        StringBuilder str = new(
+            TagNames.ContainsKey(item.Tag) ? TagNames[item.Tag] : $"{item.Tag:X4}"
+        );
+        int count = 1;
+        if (item.Value.IsGenericList())
+            count = ((IList)item.Value).Count;
+        if (item.Type == ExifType.Ascii)
+            str.AppendFormat(" {0}", item.Value as string);
+        else if ((item.Type == ExifType.Undefined || item.Type == ExifType.Byte) && count > 8)
+        {
+            str.Append($" [{count} bytes]");
+        }
+        else if (item.Value is IList lst)
+        {
+            for (int n = 0; n < count; ++n)
+            {
+                str.AppendFormat(" {0}", lst[n]);
+            }
+        }
+        else
+        {
+            str.AppendFormat(" {0}", item.Value);
+        }
+        return str.ToString();
+    }
+
+    public static readonly Dictionary<ExifTag, string> TagNames = new()
+    {
+        { 0x0100, "ImageWidth" },
+        { 0x0101, "ImageHeight" },
+        { 0x0102, "BitsPerSample" },
+        { 0x0103, "Compression" },
+        { 0x0106, "PhotometricInterpretation" },
+        { 0x0107, "Thresholding" },
+        { 0x010e, "ImageDescription" },
+        { 0x010f, "Make" },
+        { 0x0112, "Orientation" },
+        { 0x011a, "XResolution" },
+        { 0x011b, "YResolution" },
+        { 0x011c, "PlanarConfiguration" },
+        { 0x011e, "XPosition" },
+        { 0x011f, "YPosition" },
+        { 0x0128, "ResolutionUnit" },
+        { 0x0131, "Software" },
+        { 0x0132, "ModifyDate" },
+        { 0xa001, "ColorSpace" },
+        { 0xa002, "ExifImageWidth" },
+        { 0xa003, "ExifImageHeight" },
+        { 0xa20e, "FocalPlaneXResolution" },
+        { 0xa20f, "FocalPlaneYResolution" },
+        { 0xa210, "FocalPlaneResolutionUnit" },
+        /* Renishaw WiRE Custom Tags */
+        { 0xfea0, "WiREPosition" },
+        { 0xfea1, "WiREFoV" },
+        { 0xfea2, "WiREObjective" },
+        { 0xfea3, "WiRELUTLimits" },
+        { 0xfea4, "WiRERotationAngle" },
+        { 0xfea5, "WiRERotationCenter" },
+        { 0xfea6, "WiREZPosition" }
+    };
+}
diff --git a/dotnet/JpegUtil/JpegReader.cs b/dotnet/JpegUtil/JpegReader.cs
new file mode 100644 (file)
index 0000000..8e61dce
--- /dev/null
@@ -0,0 +1,179 @@
+namespace JpegUtil;
+
+using System;
+using System.Collections.Generic;
+using System.IO;
+
+public enum JfifMark
+{
+    SOF0 = 0xffc0, // start of frame (baseline DCT)
+    DHT = 0xffc4, // define Huffman table
+    RST0 = 0xffd0,// restart (d0 .. d7)
+    SOI = 0xffd8, // start of image
+    EOI = 0xffd9,  // end of image
+    SOS = 0xffda, // start of scan
+    DQT = 0xffdb, // define quantization table
+    APP0 = 0xffe0, // Application specific (e0..ef)
+    Comment = 0xfffe, // comment
+}
+
+public static class JfifMarkExtensions
+{
+    public static bool IsRST(this JfifMark mark)
+    {
+        int rst0 = (int)JfifMark.RST0;
+        int val = (int)mark;
+        return val >= rst0 && val < (rst0 + 8);
+    }
+    public static bool IsAPP(this JfifMark mark)
+    {
+        int app0 = (int)JfifMark.APP0;
+        int val = (int)mark;
+        return val >= app0 && val < (app0 + 0xf);
+    }
+    public static int AppN(this JfifMark mark)
+    {
+        return ((int)mark) % 16;
+    }
+    public static int RstN(this JfifMark mark)
+    {
+         return ((int)mark) % 16;
+    }
+}
+
+public delegate void Logger(string format, params object[] args);
+
+public class JpegReader : IDisposable
+{
+    private readonly BinaryReader _reader;
+    private Logger Log { get; set; }
+
+    public JpegReader(Stream stream, Logger logger)
+    {
+        _reader = new BinaryReader(stream);
+        Log = logger;
+    }
+
+    #region Disposable
+    private bool _disposed = false;
+    public void Dispose()
+    {
+        Dispose(true);
+    }
+    protected virtual void Dispose(bool disposing)
+    {
+        if (!_disposed)
+        {
+            if (disposing)
+            {
+                _reader.Dispose();
+            }
+            _disposed = true;
+        }
+    }
+    #endregion Disposable
+
+    // Read a big endian 16 bit value
+    private int ReadInt16BE()
+    {
+        var data = new byte[2];
+        _reader.Read(data, 0, 2);
+        // .net 8+: return (int)BinaryPrimitives.ReadUInt16BigEndian(data);
+        return ((data[0] << 8) | data[1]);
+    }
+    // Read a big endian 16 bit value minus the 2 bytes for this value
+    private int ReadSize() => ReadInt16BE() - 2;
+
+    private IEnumerable<ExifItem>? ReadApp0(int len)
+    {
+        IEnumerable<ExifItem>? result = null;
+        byte[] label = _reader.ReadBytes(5);
+        if (label[0] == 'J' && label[1] == 'F' && label[2] == 'I' && label[3] == 'F')
+        {
+            // read the JFIF header
+            byte[] data = _reader.ReadBytes(len - 5);
+            Log("JFIF header");
+        }
+        else if (label[0] == 'E' && label[1] == 'x' && label[2] == 'i' && label[3] == 'f')
+        {
+            Log("Exif block");
+            // read the padding byte
+            byte pad = _reader.ReadByte();
+            // read all the exif data into a buffer
+            byte[] data = _reader.ReadBytes(len - 6);
+            // parse it : Exif(data) or span
+            result = ExifItem.ExifParse(data);
+        }
+        else
+        {
+            _reader.BaseStream.Seek(-5, SeekOrigin.Current);
+        }
+        return result;
+    }
+
+    public IEnumerable<ExifItem> Exif()
+    {
+        int len = 0;
+        JfifMark mark = 0;
+        while (_reader.BaseStream.CanRead)
+        {
+            mark = (JfifMark)ReadInt16BE();
+            // Log("{0:X8} Mark {1:X4}", _reader.BaseStream.Position, (int)mark);
+            switch (mark)
+            {
+                case JfifMark.SOI:
+                    len = 0;
+                    Log("{0:X8} SOI", _reader.BaseStream.Position);
+                    break;
+                case JfifMark.DQT:
+                    len = ReadSize();
+                    Log("{0:X8} DQT {1:X4}", _reader.BaseStream.Position, len);
+                    break;
+                case JfifMark.DHT:
+                    len = ReadSize();
+                    Log("{0:X8} DHT {1:X4}", _reader.BaseStream.Position, len);
+                    break;
+                case JfifMark.SOS:
+                    len = ReadSize();
+                    Log("{0:X8} SOS {1:X4}", _reader.BaseStream.Position, len);
+                    yield break;
+                case JfifMark.SOF0:
+                {
+                    len = ReadSize();
+                    Log("{0:X8} SOF0 {1:X4}", _reader.BaseStream.Position, len);
+                    // read the frame, set len 0
+                    break;
+                }
+                default:
+                {
+                    if (mark.IsRST())
+                    {
+                        len = ReadSize();
+                        Log("{0:X8} RST{1} {2:X4}",
+                            _reader.BaseStream.Position, mark.RstN(), len);
+                    }
+                    else if (mark.IsAPP())
+                    {
+                        len = ReadSize();
+                        Log("{0:X8} APP{1} {2:X4}",
+                            _reader.BaseStream.Position, mark.AppN(), len);
+                        var items = ReadApp0(len);
+                        if (items != null)
+                        {
+                            foreach (ExifItem item in items)
+                                yield return item;
+                        }
+                        len = 0;
+                    }
+                    else
+                    {
+                        Log($"{0:X8} OOPS {(int)mark:X4}", _reader.BaseStream.Position);
+                        throw new Exception($"oops {(int)mark:X4}");
+                    }
+                    break;
+                }
+            }
+            _reader.BaseStream.Seek(len, SeekOrigin.Current);
+        }
+    }
+}
\ No newline at end of file
diff --git a/dotnet/JpegUtil/JpegUtil.csproj b/dotnet/JpegUtil/JpegUtil.csproj
new file mode 100644 (file)
index 0000000..664e2b1
--- /dev/null
@@ -0,0 +1,10 @@
+<Project Sdk="Microsoft.NET.Sdk">
+
+  <PropertyGroup>
+    <TargetFramework>netstandard2.0</TargetFramework>
+    <LangVersion>12.0</LangVersion>
+    <ImplicitUsings>false</ImplicitUsings>
+    <Nullable>enable</Nullable>
+  </PropertyGroup>
+
+</Project>