Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions deploy/portainer-stack.yml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
services:
beacon-server:
image: beacon-server:0.1.12
image: beacon-server:0.1.13
restart: always
environment:
ASPNETCORE_ENVIRONMENT: Production
Expand Down Expand Up @@ -59,7 +59,7 @@ services:
/bin/sh -c '(while sleep 86400; do nginx -s reload; done) & exec nginx -g "daemon off;"'

beacon-backup:
image: beacon-backup:0.1.12
image: beacon-backup:0.1.13
restart: always
environment:
BACKUP_INTERVAL_SECONDS: "86400"
Expand Down
2 changes: 1 addition & 1 deletion src/Beacon.Plugin/Beacon.Plugin.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
<!-- Drives the built DLL, the generated manifest, and the folder Dalamud loads it from. -->
<AssemblyName>Beacon</AssemblyName>
<RootNamespace>Beacon</RootNamespace>
<Version>0.1.12.0</Version>
<Version>0.1.13.0</Version>
<!-- The Dalamud SDK does not turn these on, and every service file wants System.Threading.Tasks. -->
<ImplicitUsings>enable</ImplicitUsings>
</PropertyGroup>
Expand Down
24 changes: 18 additions & 6 deletions src/Beacon.Plugin/UI/ProfileCard.cs
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,11 @@ public sealed class ProfileCard(ImageCache images)
/// </summary>
public IDalamudTextureWrap? PendingPortrait { get; set; }

/// <summary>The crop being applied to <see cref="PendingPortrait"/>, so the preview matches the editor.</summary>
public Vector2 PendingPortraitUv0 { get; set; } = Vector2.Zero;

public Vector2 PendingPortraitUv1 { get; set; } = Vector2.One;

/// <summary>Draws the card face.</summary>
public void Draw(ProfileDto profile, float scale) => DrawCardContents(profile, scale);

Expand Down Expand Up @@ -250,14 +255,21 @@ private void DrawPortrait(ProfileDto profile, float size, float scale)

var origin = ImGui.GetCursorScreenPos();
var draw = ImGui.GetWindowDrawList();
var box = new Vector2(origin.X + size, origin.Y + (size * 1.25f));
var height = size / ProfileLimits.PortraitAspect;
var box = new Vector2(origin.X + size, origin.Y + height);

var portrait = PendingPortrait
?? (profile.HasPortrait ? images.Get(profile.PortraitImageId!.Value, thumb: false) : null);

if (portrait is not null)
{
draw.AddImage(portrait.Handle, origin, box);
// A stored portrait is already cropped to shape; a pending one is the raw file and carries
// the crop the editor is previewing.
var (uv0, uv1) = ReferenceEquals(portrait, PendingPortrait)
? (PendingPortraitUv0, PendingPortraitUv1)
: (Vector2.Zero, Vector2.One);

draw.AddImage(portrait.Handle, origin, box, uv0, uv1);
}
else if (profile.HasPortrait)
{
Expand All @@ -266,7 +278,7 @@ private void DrawPortrait(ProfileDto profile, float size, float scale)
const string Fetching = "Loading...";
var fetchingSize = ImGui.CalcTextSize(Fetching);
draw.AddText(
new Vector2(origin.X + ((size - fetchingSize.X) / 2f), origin.Y + ((size * 1.25f - fetchingSize.Y) / 2f)),
new Vector2(origin.X + ((size - fetchingSize.X) / 2f), origin.Y + ((height - fetchingSize.Y) / 2f)),
Theme.InkFaint.Packed(),
Fetching);
}
Expand All @@ -276,7 +288,7 @@ private void DrawPortrait(ProfileDto profile, float size, float scale)
const string Label = "No likeness";
var textSize = ImGui.CalcTextSize(Label);
draw.AddText(
new Vector2(origin.X + ((size - textSize.X) / 2f), origin.Y + ((size * 1.25f - textSize.Y) / 2f)),
new Vector2(origin.X + ((size - textSize.X) / 2f), origin.Y + ((height - textSize.Y) / 2f)),
Theme.InkFaint.Packed(),
Label);
}
Expand All @@ -286,7 +298,7 @@ private void DrawPortrait(ProfileDto profile, float size, float scale)
// Clicking the likeness opens it at a size you can actually look at.
if (portrait is not null && profile.HasPortrait)
{
ImGui.InvisibleButton($"##portrait{profile.Id}", new Vector2(size, size * 1.25f));
ImGui.InvisibleButton($"##portrait{profile.Id}", new Vector2(size, height));

if (ImGui.IsItemHovered())
{
Expand All @@ -299,7 +311,7 @@ private void DrawPortrait(ProfileDto profile, float size, float scale)
OpenImage?.Invoke(profile.PortraitImageId!.Value);
}
else
ImGui.Dummy(new Vector2(size, size * 1.25f));
ImGui.Dummy(new Vector2(size, height));

if (profile.Gallery.Count > 1)
{
Expand Down
12 changes: 11 additions & 1 deletion src/Beacon.Plugin/Windows/ChronicleWindow.cs
Original file line number Diff line number Diff line change
Expand Up @@ -368,7 +368,17 @@ private void DrawThumbnail(ImDrawListPtr draw, ProfileDto profile, Vector2 at, f

if (profile.HasPortrait && images.Get(profile.PortraitImageId!.Value) is { } texture)
{
draw.AddImage(texture.Handle, at, box);
// The row is square and a portrait is not, so take the middle of it rather than squashing
// a face to fit.
var visible = ProfileLimits.PortraitAspect;
var inset = (1f - visible) / 2f;

draw.AddImage(
texture.Handle,
at,
box,
new Vector2(0f, inset),
new Vector2(1f, 1f - inset));
}
else
{
Expand Down
17 changes: 13 additions & 4 deletions src/Beacon.Plugin/Windows/ProfileEditorWindow.cs
Original file line number Diff line number Diff line change
Expand Up @@ -19,9 +19,10 @@ namespace Beacon.Windows;
/// </summary>
public sealed class ProfileEditorWindow : Window, IDisposable
{
private const float PortraitAspect = 5f / 6f;
private const int PortraitOutputWidth = 600;
private const int PortraitOutputHeight = 720;
// One shape for the crop preview, the stored image and the card.
private const float PortraitAspect = ProfileLimits.PortraitAspect;
private const int PortraitOutputWidth = ProfileLimits.PortraitWidth;
private const int PortraitOutputHeight = ProfileLimits.PortraitHeight;
private const int PortraitSourceMaxBytes = 25 * 1024 * 1024;
private const long PortraitSourceMaxPixels = 50_000_000;

Expand Down Expand Up @@ -905,8 +906,16 @@ private void DrawPreview(float scale)
Theme.PushPage();
if (ImGui.BeginChild("##previewPage", new Vector2(0, height), true))
{
// The picture being considered, not the one it is about to replace.
// The picture being considered, not the one it is about to replace -- carrying the crop
// the editor is showing, so the preview frames it exactly as the upload will.
preview.PendingPortrait = pendingPortraitTexture;

if (pendingPortraitTexture is not null)
{
var (uv0, uv1) = CropUvs(pendingPortraitTexture);
preview.PendingPortraitUv0 = uv0;
preview.PendingPortraitUv1 = uv1;
}
preview.Draw(BuildDraft(), scale);
}

Expand Down
14 changes: 14 additions & 0 deletions src/Beacon.Shared/Profiles/ProfileLimits.cs
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,20 @@ public static class ProfileLimits
/// <summary>All four mature themes may be set; the cap exists only to bound a malformed request.</summary>
public const int MaxMatureThemes = 4;

/// <summary>
/// The shape of a portrait, in one place.
///
/// The crop preview, the stored image and the card all have to agree on this. When they did not,
/// a portrait cropped to 5:6 was drawn into a 4:5 frame and every face on every card was quietly
/// stretched by four per cent -- which is exactly the kind of wrongness nobody can name but
/// everybody can see.
/// </summary>
public const int PortraitWidth = 600;
public const int PortraitHeight = 720;

/// <summary>Width over height, so a frame is <c>width / PortraitAspect</c> tall.</summary>
public const float PortraitAspect = PortraitWidth / (float)PortraitHeight;

/// <summary>Gallery size. Enough for a proper reference sheet, bounded enough to host.</summary>
public const int MaxGalleryImages = 8;
public const int CaptionMaxLength = 80;
Expand Down
23 changes: 21 additions & 2 deletions tools/verify-release.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -61,8 +61,8 @@ try {
foreach ($required in @(
'CreateFromImageAsync',
'CropToPngAsync',
'PortraitOutputWidth = 600',
'PortraitOutputHeight = 720',
'PortraitOutputWidth = ProfileLimits.PortraitWidth',
'PortraitOutputHeight = ProfileLimits.PortraitHeight',
'PortraitSourceMaxBytes = 25 * 1024 * 1024',
'draw.AddImage(texture.Handle, origin, box, uv0, uv1)'
)) {
Expand All @@ -71,6 +71,25 @@ try {
}
}

# The crop preview, the stored image and the card have to frame a portrait identically. When they
# did not, every face on every card was drawn 4% taller than it was cropped, which is small enough
# to go unnamed and large enough to be seen. Guarded here because three files have to agree.
$profileLimits = Get-Content -LiteralPath 'src/Beacon.Shared/Profiles/ProfileLimits.cs' -Raw
foreach ($required in @(
'PortraitWidth = 600',
'PortraitHeight = 720',
'PortraitAspect = PortraitWidth / (float)PortraitHeight'
)) {
if ($profileLimits.IndexOf($required, [StringComparison]::Ordinal) -lt 0) {
throw "The shared portrait shape is incomplete: $required"
}
}

$profileCard = Get-Content -LiteralPath 'src/Beacon.Plugin/UI/ProfileCard.cs' -Raw
if ($profileCard.IndexOf('size / ProfileLimits.PortraitAspect', [StringComparison]::Ordinal) -lt 0) {
throw 'The card must frame a portrait at the shape it is cropped to, not a hardcoded ratio.'
}

$screenshotService = Get-Content -LiteralPath 'src/Beacon.Plugin/Services/ScreenshotService.cs' -Raw
foreach ($required in @('CreateFromExistingTextureAsync', 'Uv0 = uv0', 'Uv1 = uv1', 'NewWidth = width', 'NewHeight = height')) {
if ($screenshotService.IndexOf($required, [StringComparison]::Ordinal) -lt 0) {
Expand Down
Loading