initial commit

This commit is contained in:
Renjaya Raga Zenta 2025-07-27 16:02:56 +07:00
commit c92383721d
16 changed files with 786 additions and 0 deletions

234
.editorconfig Normal file
View 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
View file

@ -0,0 +1,5 @@
bin/
obj/
/packages/
riderModule.iml
/_ReSharper.Caches/

13
.idea/.idea.stitchaton/.idea/.gitignore generated vendored Normal file
View 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

View 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>

View 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
View 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>

View 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>

View 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();

View 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"
}
}
}
}

View 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
View 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;
}
}

View file

@ -0,0 +1,8 @@
{
"Logging": {
"LogLevel": {
"Default": "Debug",
"Microsoft.AspNetCore": "Warning"
}
}
}

View file

@ -0,0 +1,9 @@
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
}
},
"AllowedHosts": "*"
}

View 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
View 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

View 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>