From: Pat Thoyts Date: Sun, 14 Dec 2025 22:26:01 +0000 (+0000) Subject: C# implementation of JPEG and EXIF reader X-Git-Url: https://git.privyetmir.co.uk/?a=commitdiff_plain;h=db3340eca3fad47e92b06e493abb439655d15cf9;p=srfdump C# implementation of JPEG and EXIF reader --- diff --git a/dotnet/.editorconfig b/dotnet/.editorconfig new file mode 100644 index 0000000..668ff28 --- /dev/null +++ b/dotnet/.editorconfig @@ -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 index 0000000..fab8f5d --- /dev/null +++ b/dotnet/.gitignore @@ -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 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 index 0000000..85d009e --- /dev/null +++ b/dotnet/JpegUtil.Test/JpegReaderTests.cs @@ -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 index 0000000..20922d7 --- /dev/null +++ b/dotnet/JpegUtil.Test/JpegUtil.Test.csproj @@ -0,0 +1,26 @@ + + + + net9.0 + enable + enable + false + + + + + + + + + + + + + + + + + + + diff --git a/dotnet/JpegUtil.sln b/dotnet/JpegUtil.sln new file mode 100644 index 0000000..fceb358 --- /dev/null +++ b/dotnet/JpegUtil.sln @@ -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 index 0000000..46720f0 --- /dev/null +++ b/dotnet/JpegUtil/Exif.cs @@ -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 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(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(); + 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(); + 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>(); + for (int n = 0; n < count; ++n) + { + lst.Add(new Tuple( + 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(); + 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>(); + for (int n = 0; n < count; ++n) + { + lst.Add(new Tuple( + 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 ExifParse(byte[] data) + { + List 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 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 index 0000000..8e61dce --- /dev/null +++ b/dotnet/JpegUtil/JpegReader.cs @@ -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? ReadApp0(int len) + { + IEnumerable? 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 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 index 0000000..664e2b1 --- /dev/null +++ b/dotnet/JpegUtil/JpegUtil.csproj @@ -0,0 +1,10 @@ + + + + netstandard2.0 + 12.0 + false + enable + + +