initial commit
This commit is contained in:
commit
c92383721d
16 changed files with 786 additions and 0 deletions
234
.editorconfig
Normal file
234
.editorconfig
Normal file
|
|
@ -0,0 +1,234 @@
|
||||||
|
# Remove the line below if you want to inherit .editorconfig settings from higher directories
|
||||||
|
root = true
|
||||||
|
|
||||||
|
[*]
|
||||||
|
# Indentation and spacing
|
||||||
|
indent_size = 2
|
||||||
|
indent_style = space
|
||||||
|
tab_width = 2
|
||||||
|
# Prevent automatically adding a UTF-8 BOM (Byte Order Mark) to files
|
||||||
|
charset = utf-8
|
||||||
|
|
||||||
|
# C# files
|
||||||
|
[*.cs]
|
||||||
|
|
||||||
|
#### Core EditorConfig Options ####
|
||||||
|
|
||||||
|
# New line preferences
|
||||||
|
end_of_line = lf
|
||||||
|
insert_final_newline = true
|
||||||
|
|
||||||
|
#### .NET Coding Conventions ####
|
||||||
|
|
||||||
|
# Organize usings
|
||||||
|
dotnet_separate_import_directive_groups = false
|
||||||
|
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_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:silent
|
||||||
|
dotnet_style_prefer_compound_assignment = true:suggestion
|
||||||
|
dotnet_style_prefer_conditional_expression_over_assignment = true:silent
|
||||||
|
dotnet_style_prefer_conditional_expression_over_return = true:silent
|
||||||
|
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:suggestion
|
||||||
|
|
||||||
|
# Parameter preferences
|
||||||
|
dotnet_code_quality_unused_parameters = all:suggestion
|
||||||
|
|
||||||
|
# Suppression preferences
|
||||||
|
dotnet_remove_unnecessary_suppression_exclusions = none
|
||||||
|
|
||||||
|
#### C# Coding Conventions ####
|
||||||
|
|
||||||
|
# var preferences
|
||||||
|
csharp_style_var_elsewhere = false:suggestion
|
||||||
|
csharp_style_var_for_built_in_types = false:suggestion
|
||||||
|
csharp_style_var_when_type_is_apparent = true:suggestion
|
||||||
|
|
||||||
|
# object creation
|
||||||
|
csharp_style_object_creation_when_type_is_apparent = true:suggestion
|
||||||
|
|
||||||
|
# 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:silent
|
||||||
|
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_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_local_function = true:suggestion
|
||||||
|
csharp_preferred_modifier_order = public,private,protected,internal,static,extern,new,virtual,abstract,sealed,override,readonly,unsafe,volatile,async:silent
|
||||||
|
|
||||||
|
# Code-block preferences
|
||||||
|
csharp_prefer_braces = true:silent
|
||||||
|
csharp_prefer_simple_using_statement = true:suggestion
|
||||||
|
|
||||||
|
# Expression-level preferences
|
||||||
|
csharp_prefer_simple_default_expression = true:suggestion
|
||||||
|
csharp_style_deconstructed_variable_declaration = true:suggestion
|
||||||
|
csharp_style_inlined_variable_declaration = true:suggestion
|
||||||
|
csharp_style_pattern_local_over_anonymous_function = true:suggestion
|
||||||
|
csharp_style_prefer_index_operator = true:suggestion
|
||||||
|
csharp_style_prefer_range_operator = 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 = true
|
||||||
|
csharp_indent_labels = no_change
|
||||||
|
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 = false
|
||||||
|
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 = control_flow_statements,expressions
|
||||||
|
csharp_space_between_square_brackets = false
|
||||||
|
|
||||||
|
# Wrapping preferences
|
||||||
|
csharp_preserve_single_line_blocks = true
|
||||||
|
csharp_preserve_single_line_statements = true
|
||||||
|
|
||||||
|
#### Naming styles ####
|
||||||
|
|
||||||
|
### Naming rules
|
||||||
|
|
||||||
|
dotnet_naming_rule.interface_should_be_begins_with_i.severity = suggestion
|
||||||
|
dotnet_naming_rule.interface_should_be_begins_with_i.symbols = interfaces
|
||||||
|
dotnet_naming_rule.interface_should_be_begins_with_i.style = begins_with_i
|
||||||
|
|
||||||
|
dotnet_naming_rule.should_be_pascal_case.severity = suggestion
|
||||||
|
dotnet_naming_rule.should_be_pascal_case.symbols = should_be_pascal_case
|
||||||
|
dotnet_naming_rule.should_be_pascal_case.style = should_be_pascal_case
|
||||||
|
|
||||||
|
dotnet_naming_rule.private_fields_underscored.severity = suggestion
|
||||||
|
dotnet_naming_rule.private_fields_underscored.symbols = private_fields
|
||||||
|
dotnet_naming_rule.private_fields_underscored.style = private_fields
|
||||||
|
|
||||||
|
dotnet_naming_rule.local_variables_should_be_camel_case.severity = suggestion
|
||||||
|
dotnet_naming_rule.local_variables_should_be_camel_case.symbols = local_variables
|
||||||
|
dotnet_naming_rule.local_variables_should_be_camel_case.style = local_variables
|
||||||
|
|
||||||
|
dotnet_naming_rule.constants_should_be_screaming_snake_case.severity = suggestion
|
||||||
|
dotnet_naming_rule.constants_should_be_screaming_snake_case.symbols = constants
|
||||||
|
dotnet_naming_rule.constants_should_be_screaming_snake_case.style = screaming_snake_case
|
||||||
|
|
||||||
|
### 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.should_be_pascal_case.applicable_kinds = class, struct, interface, enum, event, delegate, method, property
|
||||||
|
dotnet_naming_symbols.should_be_pascal_case.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected
|
||||||
|
dotnet_naming_symbols.should_be_pascal_case.required_modifiers =
|
||||||
|
|
||||||
|
dotnet_naming_symbols.private_fields.applicable_kinds = field
|
||||||
|
dotnet_naming_symbols.private_fields.applicable_accessibilities = private
|
||||||
|
|
||||||
|
dotnet_naming_symbols.local_variables.applicable_kinds = local
|
||||||
|
dotnet_naming_symbols.local_variables.applicable_accessibilities = local
|
||||||
|
|
||||||
|
dotnet_naming_symbols.constants.applicable_kinds = field
|
||||||
|
dotnet_naming_symbols.constants.applicable_accessibilities = private
|
||||||
|
dotnet_naming_symbols.constants.required_modifiers = const
|
||||||
|
|
||||||
|
### Naming styles
|
||||||
|
|
||||||
|
dotnet_naming_style.begins_with_i.required_prefix = I
|
||||||
|
dotnet_naming_style.begins_with_i.required_suffix =
|
||||||
|
dotnet_naming_style.begins_with_i.word_separator =
|
||||||
|
dotnet_naming_style.begins_with_i.capitalization = pascal_case
|
||||||
|
|
||||||
|
dotnet_naming_style.should_be_pascal_case.required_prefix =
|
||||||
|
dotnet_naming_style.should_be_pascal_case.required_suffix =
|
||||||
|
dotnet_naming_style.should_be_pascal_case.word_separator =
|
||||||
|
dotnet_naming_style.should_be_pascal_case.capitalization = pascal_case
|
||||||
|
|
||||||
|
dotnet_naming_style.private_fields.required_prefix = _
|
||||||
|
dotnet_naming_style.private_fields.capitalization = camel_case
|
||||||
|
|
||||||
|
dotnet_naming_style.screaming_snake_case.capitalization = all_upper
|
||||||
|
dotnet_naming_style.screaming_snake_case.word_separator = _
|
||||||
|
|
||||||
|
dotnet_naming_style.local_variables.capitalization = camel_case
|
||||||
5
.gitignore
vendored
Normal file
5
.gitignore
vendored
Normal file
|
|
@ -0,0 +1,5 @@
|
||||||
|
bin/
|
||||||
|
obj/
|
||||||
|
/packages/
|
||||||
|
riderModule.iml
|
||||||
|
/_ReSharper.Caches/
|
||||||
13
.idea/.idea.stitchaton/.idea/.gitignore
generated
vendored
Normal file
13
.idea/.idea.stitchaton/.idea/.gitignore
generated
vendored
Normal file
|
|
@ -0,0 +1,13 @@
|
||||||
|
# Default ignored files
|
||||||
|
/shelf/
|
||||||
|
/workspace.xml
|
||||||
|
# Rider ignored files
|
||||||
|
/projectSettingsUpdater.xml
|
||||||
|
/.idea.stitchaton.iml
|
||||||
|
/modules.xml
|
||||||
|
/contentModel.xml
|
||||||
|
# Editor-based HTTP Client requests
|
||||||
|
/httpRequests/
|
||||||
|
# Datasource local storage ignored files
|
||||||
|
/dataSources/
|
||||||
|
/dataSources.local.xml
|
||||||
4
.idea/.idea.stitchaton/.idea/encodings.xml
generated
Normal file
4
.idea/.idea.stitchaton/.idea/encodings.xml
generated
Normal file
|
|
@ -0,0 +1,4 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="Encoding" addBOMForNewFiles="with BOM under Windows, with no BOM otherwise" />
|
||||||
|
</project>
|
||||||
8
.idea/.idea.stitchaton/.idea/indexLayout.xml
generated
Normal file
8
.idea/.idea.stitchaton/.idea/indexLayout.xml
generated
Normal file
|
|
@ -0,0 +1,8 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="UserContentModel">
|
||||||
|
<attachedFolders />
|
||||||
|
<explicitIncludes />
|
||||||
|
<explicitExcludes />
|
||||||
|
</component>
|
||||||
|
</project>
|
||||||
6
.idea/.idea.stitchaton/.idea/vcs.xml
generated
Normal file
6
.idea/.idea.stitchaton/.idea/vcs.xml
generated
Normal file
|
|
@ -0,0 +1,6 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="VcsDirectoryMappings">
|
||||||
|
<mapping directory="$PROJECT_DIR$" vcs="Git" />
|
||||||
|
</component>
|
||||||
|
</project>
|
||||||
18
src/Oh.My.Stitcher/Oh.My.Stitcher.csproj
Normal file
18
src/Oh.My.Stitcher/Oh.My.Stitcher.csproj
Normal file
|
|
@ -0,0 +1,18 @@
|
||||||
|
<Project Sdk="Microsoft.NET.Sdk.Web">
|
||||||
|
|
||||||
|
<PropertyGroup>
|
||||||
|
<TargetFramework>net8.0</TargetFramework>
|
||||||
|
<Nullable>enable</Nullable>
|
||||||
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
|
<InvariantGlobalization>true</InvariantGlobalization>
|
||||||
|
<PublishAot>true</PublishAot>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<PackageReference Include="NetVips" Version="3.1.0" />
|
||||||
|
<PackageReference Include="NetVips.Native.linux-x64" Version="8.17.1" />
|
||||||
|
<PackageReference Include="Validation" Version="2.6.68" />
|
||||||
|
<PackageReference Include="ZLogger" Version="2.5.10" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
</Project>
|
||||||
55
src/Oh.My.Stitcher/Program.cs
Normal file
55
src/Oh.My.Stitcher/Program.cs
Normal file
|
|
@ -0,0 +1,55 @@
|
||||||
|
using System.IO.Pipelines;
|
||||||
|
using Microsoft.AspNetCore.Http.Json;
|
||||||
|
using NetVips;
|
||||||
|
using Oh.My.Stitcher;
|
||||||
|
using Validation;
|
||||||
|
using ZLogger;
|
||||||
|
|
||||||
|
WebApplicationBuilder builder = WebApplication.CreateSlimBuilder(args);
|
||||||
|
builder.Logging.ClearProviders().AddZLoggerConsole();
|
||||||
|
builder.Services.Configure<JsonOptions>(options =>
|
||||||
|
{
|
||||||
|
options.SerializerOptions.TypeInfoResolver = StitchSerializerContext.Default;
|
||||||
|
});
|
||||||
|
WebApplication app = builder.Build();
|
||||||
|
|
||||||
|
ILoggerFactory loggerFactory = app.Services.GetRequiredService<ILoggerFactory>();
|
||||||
|
ILogger logger = loggerFactory.CreateLogger<Program>();
|
||||||
|
|
||||||
|
string? tilesDirectory = Environment.GetEnvironmentVariable("ASSET_PATH_RO");
|
||||||
|
|
||||||
|
// sanity check
|
||||||
|
Assumes.NotNullOrEmpty(tilesDirectory);
|
||||||
|
Assumes.True(File.Exists(Path.Combine(tilesDirectory, "A1.png")));
|
||||||
|
Assumes.True(File.Exists(Path.Combine(tilesDirectory, "AE55.png")));
|
||||||
|
|
||||||
|
app.UseDefaultFiles();
|
||||||
|
app.UseStaticFiles();
|
||||||
|
app.MapPost("/api/image/generate", (Stitch request) =>
|
||||||
|
{
|
||||||
|
Pipe pipe = new();
|
||||||
|
_ = Task.Run(async () =>
|
||||||
|
{
|
||||||
|
List<Image> images = [];
|
||||||
|
try
|
||||||
|
{
|
||||||
|
using Image image = Tile.Create(in request, tilesDirectory, images, logger);
|
||||||
|
image.WriteToStream(pipe.Writer.AsStream(), ".png");
|
||||||
|
}
|
||||||
|
catch( Exception e )
|
||||||
|
{
|
||||||
|
logger.ZLogError(e, $"Error when generating image");
|
||||||
|
using Image errorImage = Tile.CreateError(e);
|
||||||
|
errorImage.WriteToStream(pipe.Writer.AsStream(), ".png");
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
foreach( Image img in images )
|
||||||
|
img.Dispose();
|
||||||
|
await pipe.Writer.CompleteAsync();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return Results.Stream(pipe.Reader.AsStream(), "image/png");
|
||||||
|
});
|
||||||
|
|
||||||
|
app.Run();
|
||||||
15
src/Oh.My.Stitcher/Properties/launchSettings.json
Normal file
15
src/Oh.My.Stitcher/Properties/launchSettings.json
Normal file
|
|
@ -0,0 +1,15 @@
|
||||||
|
{
|
||||||
|
"$schema": "http://json.schemastore.org/launchsettings.json",
|
||||||
|
"profiles": {
|
||||||
|
"http": {
|
||||||
|
"commandName": "Project",
|
||||||
|
"dotnetRunMessages": true,
|
||||||
|
"launchBrowser": true,
|
||||||
|
"applicationUrl": "http://localhost:5108",
|
||||||
|
"environmentVariables": {
|
||||||
|
"ASPNETCORE_ENVIRONMENT": "Development",
|
||||||
|
"ASSET_PATH_RO": "/home/formulatrix/Downloads/tiles1705"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
67
src/Oh.My.Stitcher/Stitch.cs
Normal file
67
src/Oh.My.Stitcher/Stitch.cs
Normal file
|
|
@ -0,0 +1,67 @@
|
||||||
|
using System.Text.Json;
|
||||||
|
using System.Text.Json.Serialization;
|
||||||
|
|
||||||
|
namespace Oh.My.Stitcher;
|
||||||
|
|
||||||
|
public readonly record struct CropOffset(float X, float Y);
|
||||||
|
public readonly record struct CropSize(float Width, float Height);
|
||||||
|
|
||||||
|
public readonly record struct Stitch(
|
||||||
|
[property: JsonPropertyName("canvas_rect")]
|
||||||
|
string CanvasRect,
|
||||||
|
[property: JsonPropertyName("crop_offset"), JsonConverter(typeof(CropOffsetConverter))]
|
||||||
|
CropOffset CropOffset,
|
||||||
|
[property: JsonPropertyName("crop_size"), JsonConverter(typeof(CropSizeConverter))]
|
||||||
|
CropSize CropSize,
|
||||||
|
[property: JsonPropertyName("output_scale")]
|
||||||
|
float OutputScale
|
||||||
|
);
|
||||||
|
|
||||||
|
[JsonSerializable(typeof(Stitch))]
|
||||||
|
internal partial class StitchSerializerContext : JsonSerializerContext;
|
||||||
|
|
||||||
|
public class CropOffsetConverter : JsonConverter<CropOffset>
|
||||||
|
{
|
||||||
|
public override CropOffset Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
|
||||||
|
{
|
||||||
|
if( reader.TokenType != JsonTokenType.StartArray ) throw new JsonException();
|
||||||
|
reader.Read();
|
||||||
|
float x = reader.GetSingle();
|
||||||
|
reader.Read();
|
||||||
|
float y = reader.GetSingle();
|
||||||
|
reader.Read();
|
||||||
|
if( reader.TokenType != JsonTokenType.EndArray ) throw new JsonException();
|
||||||
|
return new CropOffset(x, y);
|
||||||
|
}
|
||||||
|
|
||||||
|
public override void Write(Utf8JsonWriter writer, CropOffset value, JsonSerializerOptions options)
|
||||||
|
{
|
||||||
|
writer.WriteStartArray();
|
||||||
|
writer.WriteNumberValue(value.X);
|
||||||
|
writer.WriteNumberValue(value.Y);
|
||||||
|
writer.WriteEndArray();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public class CropSizeConverter : JsonConverter<CropSize>
|
||||||
|
{
|
||||||
|
public override CropSize Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
|
||||||
|
{
|
||||||
|
if( reader.TokenType != JsonTokenType.StartArray ) throw new JsonException();
|
||||||
|
reader.Read();
|
||||||
|
float width = reader.GetSingle();
|
||||||
|
reader.Read();
|
||||||
|
float height = reader.GetSingle();
|
||||||
|
reader.Read();
|
||||||
|
if( reader.TokenType != JsonTokenType.EndArray ) throw new JsonException();
|
||||||
|
return new CropSize(width, height);
|
||||||
|
}
|
||||||
|
|
||||||
|
public override void Write(Utf8JsonWriter writer, CropSize value, JsonSerializerOptions options)
|
||||||
|
{
|
||||||
|
writer.WriteStartArray();
|
||||||
|
writer.WriteNumberValue(value.Width);
|
||||||
|
writer.WriteNumberValue(value.Height);
|
||||||
|
writer.WriteEndArray();
|
||||||
|
}
|
||||||
|
}
|
||||||
105
src/Oh.My.Stitcher/Tile.cs
Normal file
105
src/Oh.My.Stitcher/Tile.cs
Normal file
|
|
@ -0,0 +1,105 @@
|
||||||
|
// ReSharper disable ReplaceSliceWithRangeIndexer
|
||||||
|
|
||||||
|
using NetVips;
|
||||||
|
using ZLogger;
|
||||||
|
|
||||||
|
namespace Oh.My.Stitcher;
|
||||||
|
|
||||||
|
public static class Tile
|
||||||
|
{
|
||||||
|
public static Image Create(in Stitch request, string tilesDirectory, List<Image> images, ILogger logger)
|
||||||
|
{
|
||||||
|
if( !TryParseRect(request.CanvasRect, out int minRow, out int maxRow, out int minCol, out int maxCol) )
|
||||||
|
throw new ArgumentException($"Invalid canvas_rect: '{request.CanvasRect}'");
|
||||||
|
|
||||||
|
logger.ZLogDebug(
|
||||||
|
$"rect: {request.CanvasRect}, minRow: {minRow}, maxRow: {maxRow}, minCol: {minCol}, maxCol: {maxCol}");
|
||||||
|
int width = maxCol - minCol + 1;
|
||||||
|
for( int row = minRow; row <= maxRow; row++ )
|
||||||
|
for( int col = minCol; col <= maxCol; col++ )
|
||||||
|
images.Add(Image.NewFromFile(FullPath(tilesDirectory, row, col)));
|
||||||
|
|
||||||
|
using var canvasImage = Image.Arrayjoin(images.ToArray(), width);
|
||||||
|
int cropLeft = (int)( canvasImage.Width * request.CropOffset.X );
|
||||||
|
int cropTop = (int)( canvasImage.Height * request.CropOffset.Y );
|
||||||
|
int cropWidth = (int)( canvasImage.Width * request.CropSize.Width );
|
||||||
|
int cropHeight = (int)( canvasImage.Height * request.CropSize.Height );
|
||||||
|
|
||||||
|
return canvasImage.Crop(cropLeft, cropTop, cropWidth, cropHeight).Resize(request.OutputScale);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Image CreateError(Exception e)
|
||||||
|
{
|
||||||
|
const int padding = 20;
|
||||||
|
using var text = Image.Text($"Error:\n{e.Message}", dpi: 96, align: Enums.Align.Low);
|
||||||
|
return text.Embed(padding, padding, text.Width + ( 2 * padding ), text.Height + ( 2 * padding ));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string FullPath(string directory, int row, int col)
|
||||||
|
{
|
||||||
|
string letterPart = "";
|
||||||
|
while (row > 0)
|
||||||
|
{
|
||||||
|
int remainder = (row - 1) % 26;
|
||||||
|
letterPart = (char)('A' + remainder) + letterPart;
|
||||||
|
row = (row - 1) / 26;
|
||||||
|
}
|
||||||
|
string fileName = $"{letterPart}{col}.png";
|
||||||
|
return Path.Combine(directory, fileName);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool TryParseRect(string rect, out int minRow, out int maxRow, out int minCol, out int maxCol)
|
||||||
|
{
|
||||||
|
minRow = maxRow = minCol = maxCol = 0;
|
||||||
|
string[] corners = rect.Split(':');
|
||||||
|
switch( corners.Length )
|
||||||
|
{
|
||||||
|
case 1:
|
||||||
|
{
|
||||||
|
if( !TryParseName(corners[0], out int r, out int c) )
|
||||||
|
return false;
|
||||||
|
|
||||||
|
minRow = maxRow = r;
|
||||||
|
minCol = maxCol = c;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
case 2:
|
||||||
|
{
|
||||||
|
if( !TryParseName(corners[0], out int r1, out int c1) || !TryParseName(corners[1], out int r2, out int c2) )
|
||||||
|
return false;
|
||||||
|
|
||||||
|
minRow = Math.Min(r1, r2);
|
||||||
|
maxRow = Math.Max(r1, r2);
|
||||||
|
minCol = Math.Min(c1, c2);
|
||||||
|
maxCol = Math.Max(c1, c2);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
default:
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool TryParseName(string name, out int row, out int col)
|
||||||
|
{
|
||||||
|
row = col = 0;
|
||||||
|
ReadOnlySpan<char> span = name.AsSpan().Trim();
|
||||||
|
int splitIndex = span.IndexOfAnyInRange('0', '9');
|
||||||
|
if( splitIndex <= 0 )
|
||||||
|
return false;
|
||||||
|
if( !int.TryParse(span.Slice(splitIndex), out col))
|
||||||
|
return false;
|
||||||
|
int letter = 0;
|
||||||
|
foreach( char c in span.Slice(0, splitIndex) )
|
||||||
|
{
|
||||||
|
char upper = char.ToUpperInvariant(c);
|
||||||
|
if( upper is < 'A' or > 'Z' )
|
||||||
|
return false;
|
||||||
|
letter = letter * 26 + ( upper - 'A' + 1 );
|
||||||
|
}
|
||||||
|
|
||||||
|
row = letter;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
8
src/Oh.My.Stitcher/appsettings.Development.json
Normal file
8
src/Oh.My.Stitcher/appsettings.Development.json
Normal file
|
|
@ -0,0 +1,8 @@
|
||||||
|
{
|
||||||
|
"Logging": {
|
||||||
|
"LogLevel": {
|
||||||
|
"Default": "Debug",
|
||||||
|
"Microsoft.AspNetCore": "Warning"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
9
src/Oh.My.Stitcher/appsettings.json
Normal file
9
src/Oh.My.Stitcher/appsettings.json
Normal file
|
|
@ -0,0 +1,9 @@
|
||||||
|
{
|
||||||
|
"Logging": {
|
||||||
|
"LogLevel": {
|
||||||
|
"Default": "Information",
|
||||||
|
"Microsoft.AspNetCore": "Warning"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"AllowedHosts": "*"
|
||||||
|
}
|
||||||
214
src/Oh.My.Stitcher/wwwroot/index.html
Normal file
214
src/Oh.My.Stitcher/wwwroot/index.html
Normal file
|
|
@ -0,0 +1,214 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>Oh My Stitcher!</title>
|
||||||
|
<style>
|
||||||
|
body {
|
||||||
|
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
|
||||||
|
background-color: #f4f7f9;
|
||||||
|
color: #333;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
margin: 0;
|
||||||
|
padding: 2rem;
|
||||||
|
}
|
||||||
|
.container {
|
||||||
|
background: white;
|
||||||
|
padding: 2rem;
|
||||||
|
border-radius: 8px;
|
||||||
|
box-shadow: 0 4px 15px rgba(0, 0, 0, 0.1);
|
||||||
|
width: 100%;
|
||||||
|
max-width: 500px;
|
||||||
|
}
|
||||||
|
h1 {
|
||||||
|
text-align: center;
|
||||||
|
color: #2c3e50;
|
||||||
|
}
|
||||||
|
form {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1fr 1fr;
|
||||||
|
gap: 1rem 1.5rem;
|
||||||
|
}
|
||||||
|
.full-width {
|
||||||
|
grid-column: 1 / -1;
|
||||||
|
}
|
||||||
|
label {
|
||||||
|
font-weight: 600;
|
||||||
|
margin-bottom: 0.25rem;
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
input[type="text"], input[type="number"] {
|
||||||
|
width: 100%;
|
||||||
|
padding: 0.75rem;
|
||||||
|
border: 1px solid #dcdcdc;
|
||||||
|
border-radius: 4px;
|
||||||
|
box-sizing: border-box;
|
||||||
|
font-size: 1rem;
|
||||||
|
}
|
||||||
|
button {
|
||||||
|
grid-column: 1 / -1;
|
||||||
|
padding: 1rem;
|
||||||
|
font-size: 1.1rem;
|
||||||
|
font-weight: 600;
|
||||||
|
color: white;
|
||||||
|
background-color: #3498db;
|
||||||
|
border: none;
|
||||||
|
border-radius: 4px;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: background-color 0.2s;
|
||||||
|
}
|
||||||
|
button:hover {
|
||||||
|
background-color: #2980b9;
|
||||||
|
}
|
||||||
|
button:disabled {
|
||||||
|
background-color: #95a5a6;
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
#output-container {
|
||||||
|
margin-top: 1.5rem;
|
||||||
|
text-align: center;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
#output-container img {
|
||||||
|
max-width: 100%;
|
||||||
|
max-height: 80vh;
|
||||||
|
height: auto;
|
||||||
|
width: auto;
|
||||||
|
border: 1px solid #dcdcdc;
|
||||||
|
border-radius: 4px;
|
||||||
|
background-color: #f8f9fa;
|
||||||
|
box-shadow: 0 2px 5px rgba(0,0,0,0.05);
|
||||||
|
}
|
||||||
|
#status {
|
||||||
|
margin-top: 1rem;
|
||||||
|
width: 100%;
|
||||||
|
max-width: 500px;
|
||||||
|
box-sizing: border-box;
|
||||||
|
text-align: center;
|
||||||
|
font-weight: 500;
|
||||||
|
padding: 0.75rem;
|
||||||
|
}
|
||||||
|
.status-error {
|
||||||
|
color: #c0392b;
|
||||||
|
background-color: #f9e2e2;
|
||||||
|
}
|
||||||
|
.status-info {
|
||||||
|
color: #2980b9;
|
||||||
|
}
|
||||||
|
.status-success {
|
||||||
|
color: #27ae60;
|
||||||
|
background-color: #eaf7f0;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="container">
|
||||||
|
<h1>Oh My Stitcher!</h1>
|
||||||
|
<form id="generator-form">
|
||||||
|
<div class="full-width">
|
||||||
|
<label for="canvasRect">Canvas Rect (e.g., A1:H12 or B5)</label>
|
||||||
|
<input type="text" id="canvasRect" value="A1:H12" required>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label for="cropOffsetX">Crop Offset X</label>
|
||||||
|
<input type="number" id="cropOffsetX" value="0.25" step="0.01" min="0" max="1">
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label for="cropOffsetY">Crop Offset Y</label>
|
||||||
|
<input type="number" id="cropOffsetY" value="0.25" step="0.01" min="0" max="1">
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label for="cropSizeWidth">Crop Size Width</label>
|
||||||
|
<input type="number" id="cropSizeWidth" value="0.5" step="0.01" min="0" max="1">
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label for="cropSizeHeight">Crop Size Height</label>
|
||||||
|
<input type="number" id="cropSizeHeight" value="0.5" step="0.01" min="0" max="1">
|
||||||
|
</div>
|
||||||
|
<div class="full-width">
|
||||||
|
<label for="outputScale">Output Scale</label>
|
||||||
|
<input type="number" id="outputScale" value="0.05" step="0.01" min="0.01" max="0.05">
|
||||||
|
</div>
|
||||||
|
<button type="submit" id="generate-btn">Generate Image</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
<div id="status"></div>
|
||||||
|
<div id="output-container"></div>
|
||||||
|
<script>
|
||||||
|
const form = document.getElementById('generator-form');
|
||||||
|
const generateBtn = document.getElementById('generate-btn');
|
||||||
|
const statusDiv = document.getElementById('status');
|
||||||
|
const outputContainer = document.getElementById('output-container');
|
||||||
|
|
||||||
|
form.addEventListener('submit', async (event) => {
|
||||||
|
event.preventDefault();
|
||||||
|
generateBtn.disabled = true;
|
||||||
|
generateBtn.textContent = 'Generating...';
|
||||||
|
statusDiv.textContent = 'Sending request...';
|
||||||
|
statusDiv.className = 'status-info';
|
||||||
|
const existingImg = outputContainer.querySelector('img');
|
||||||
|
if (existingImg) existingImg.remove();
|
||||||
|
|
||||||
|
const payload = {
|
||||||
|
canvas_rect: document.getElementById('canvasRect').value,
|
||||||
|
crop_offset: [
|
||||||
|
parseFloat(document.getElementById('cropOffsetX').value),
|
||||||
|
parseFloat(document.getElementById('cropOffsetY').value)
|
||||||
|
],
|
||||||
|
crop_size: [
|
||||||
|
parseFloat(document.getElementById('cropSizeWidth').value),
|
||||||
|
parseFloat(document.getElementById('cropSizeHeight').value)
|
||||||
|
],
|
||||||
|
output_scale: parseFloat(document.getElementById('outputScale').value)
|
||||||
|
};
|
||||||
|
|
||||||
|
const startTime = performance.now();
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch('/api/image/generate', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify(payload)
|
||||||
|
});
|
||||||
|
|
||||||
|
const headersReceivedTime = performance.now();
|
||||||
|
const timeToFirstByte = (headersReceivedTime - startTime).toFixed(1);
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
const errorData = await response.json();
|
||||||
|
throw new Error(`Server error (${response.status}): ${errorData.title || 'Unknown Error'}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
statusDiv.textContent = `Headers received in ${timeToFirstByte} ms. Downloading image...`;
|
||||||
|
|
||||||
|
const imageBlob = await response.blob();
|
||||||
|
|
||||||
|
const endTime = performance.now();
|
||||||
|
const totalDuration = (endTime - startTime).toFixed(1);
|
||||||
|
|
||||||
|
const imageUrl = URL.createObjectURL(imageBlob);
|
||||||
|
const img = document.createElement('img');
|
||||||
|
img.src = imageUrl;
|
||||||
|
img.onload = () => URL.revokeObjectURL(img.src);
|
||||||
|
|
||||||
|
outputContainer.appendChild(img);
|
||||||
|
|
||||||
|
const sizeInMB = (imageBlob.size / 1024 / 1024).toFixed(2);
|
||||||
|
statusDiv.textContent = `Success! Image (${sizeInMB} MB) received in ${totalDuration} ms.`;
|
||||||
|
statusDiv.className = 'status-success';
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Fetch error:', error);
|
||||||
|
statusDiv.textContent = error.message;
|
||||||
|
statusDiv.className = 'status-error';
|
||||||
|
} finally {
|
||||||
|
generateBtn.disabled = false;
|
||||||
|
generateBtn.textContent = 'Generate Image';
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
23
stitchaton.sln
Normal file
23
stitchaton.sln
Normal file
|
|
@ -0,0 +1,23 @@
|
||||||
|
|
||||||
|
Microsoft Visual Studio Solution File, Format Version 12.00
|
||||||
|
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{AEE9B1D3-6AD8-4EEE-800B-2873B0BB78DD}"
|
||||||
|
EndProject
|
||||||
|
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Oh.My.Stitcher", "src\Oh.My.Stitcher\Oh.My.Stitcher.csproj", "{9AB5F809-0D6A-4906-AB89-DC797FB7CF42}"
|
||||||
|
EndProject
|
||||||
|
EndProject
|
||||||
|
Global
|
||||||
|
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||||
|
Debug|Any CPU = Debug|Any CPU
|
||||||
|
Release|Any CPU = Release|Any CPU
|
||||||
|
EndGlobalSection
|
||||||
|
GlobalSection(NestedProjects) = preSolution
|
||||||
|
{9AB5F809-0D6A-4906-AB89-DC797FB7CF42} = {AEE9B1D3-6AD8-4EEE-800B-2873B0BB78DD}
|
||||||
|
{A9CC8F78-CB38-4986-9480-5FB4556F1356} = {AEE9B1D3-6AD8-4EEE-800B-2873B0BB78DD}
|
||||||
|
EndGlobalSection
|
||||||
|
GlobalSection(ProjectConfigurationPlatforms) = postSolution
|
||||||
|
{9AB5F809-0D6A-4906-AB89-DC797FB7CF42}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||||
|
{9AB5F809-0D6A-4906-AB89-DC797FB7CF42}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
|
{9AB5F809-0D6A-4906-AB89-DC797FB7CF42}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
|
{9AB5F809-0D6A-4906-AB89-DC797FB7CF42}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||||
|
EndGlobalSection
|
||||||
|
EndGlobal
|
||||||
2
stitchaton.sln.DotSettings
Normal file
2
stitchaton.sln.DotSettings
Normal file
|
|
@ -0,0 +1,2 @@
|
||||||
|
<wpf:ResourceDictionary xml:space="preserve" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:s="clr-namespace:System;assembly=mscorlib" xmlns:ss="urn:shemas-jetbrains-com:settings-storage-xaml" xmlns:wpf="http://schemas.microsoft.com/winfx/2006/xaml/presentation">
|
||||||
|
<s:Boolean x:Key="/Default/UserDictionary/Words/=Stitcher/@EntryIndexedValue">True</s:Boolean></wpf:ResourceDictionary>
|
||||||
Loading…
Add table
Add a link
Reference in a new issue