commit 2466aa3ed34ba3838f47d276d605acb27ef24f41
Author: Jens Kristoffersson <jens.kristoffersson.se@gmail.com>
AuthorDate: Tue Sep 1 10:13:48 2026 +0200
Commit: Jens Kristoffersson <jens.kristoffersson.se@gmail.com>
CommitDate: Tue Sep 1 10:13:48 2026 +0200
Fas 0: OCR-kontrakt för försättsbladet
Nedströms läses mottagningen via server-side text-OCR. Vi definierar
formatet: ankarordet REMISS + en maskinläsbar rad "HSA-ID: <id>" i
monospace på egen rad.
- OcrContract: delad formattering + tolerant parser (tål versaler/
gemener, tappat kolon/bindestreck, OCR-blanksteg i id:t, kringtext)
- CoverPageGenerator: ny OCR-anpassad layout, Arial/Consolas
- WindowsFontResolver: PdfSharp 6.1 resolvar inte namngivna typsnitt
automatiskt – vi pekar ut C:\Windows\Fonts själva
- tools/Remiss.CoverPreview: dev-verktyg för att öga-testa layouten
- docs/forsattsblad-ocr.md: kontraktet dokumenterat
- OcrContractTests (9 st), totalt 15/15 gröna
---
.gitignore | 3 +
README.md | 21 +++++--
Remiss.sln | 9 +++
docs/forsattsblad-ocr.md | 46 ++++++++++++++++
src/Remiss.Core/Pdf/CoverPageGenerator.cs | 64 +++++++++++++++-------
src/Remiss.Core/Pdf/OcrContract.cs | 49 +++++++++++++++++
src/Remiss.Core/Pdf/WindowsFontResolver.cs | 38 +++++++++++++
tests/Remiss.Core.Tests/OcrContractTests.cs | 42 ++++++++++++++
tools/Remiss.CoverPreview/Program.cs | 19 +++++++
.../Remiss.CoverPreview/Remiss.CoverPreview.csproj | 15 +++++
10 files changed, 282 insertions(+), 24 deletions(-)
diff --git a/.gitignore b/.gitignore
index 6b44f5f..a738e2c 100644
--- a/.gitignore
+++ b/.gitignore
@@ -24,6 +24,9 @@ tools/*.zip
audit/
*.jsonl
+# Generated preview / test PDFs
+*.pdf
+
# OS
Thumbs.db
desktop.ini
diff --git a/README.md b/README.md
index 693a015..a08ecb8 100644
--- a/README.md
+++ b/README.md
@@ -11,7 +11,7 @@ Se [Plan.md](Plan.md) för arkitektur, teknikval och faser.
| Fas | Innehåll | Läge |
|-----|----------|------|
-| 0 | Spika hur nedströms-skrivaren läser av mottagning | **Blockerad – ej påbörjad** |
+| 0 | Spika hur nedströms-skrivaren läser av mottagning | **Klart: text-OCR server-side. [Kontrakt](docs/forsattsblad-ocr.md).** |
| 1 | Fristående C#-app, körs manuellt med test-PDF | **Bygger + tester gröna. Kvar: manuellt dialog+utskriftstest.** |
| 2 | Koppla på fångst (clawPDF) | Ej påbörjad |
| 3 | Härdning (HSA, config, audit, temp, felflöden) | Delvis förberedd i koden |
@@ -28,6 +28,8 @@ Se [Plan.md](Plan.md) för arkitektur, teknikval och faser.
- SumatraPDF-binär i `tools/` – se [tools/README.md](tools/README.md).
Behövs bara för det sista utskriftssteget; dialog + försättsblad + merge
fungerar utan.
+- Typsnitten **Arial** och **Consolas** (standard på Windows) – försättsbladet
+ renderas mot dem via en egen `WindowsFontResolver`.
## Bygga och köra (Fas 1)
@@ -45,6 +47,12 @@ Målskrivaren styrs av `targetPrinter` i `config/appsettings.json`
Utan SumatraPDF-binär kommer stegen dialog → försättsblad → merge att köras,
och utskriftssteget kastar ett tydligt fel.
+Iterera på försättsbladets OCR-layout utan hela flödet:
+
+```powershell
+dotnet run --project tools/Remiss.CoverPreview -- preview.pdf SE2321000131-E000000000009 "Testmottagning"
+```
+
## Projektstruktur
```
@@ -52,13 +60,17 @@ Remiss.sln
config/
appsettings.json sökvägar, målskrivare, retry, flaggor
mottagningar.json POC-register (namn → HSA-id)
-tools/ buntad SumatraPDF (checkas ej in)
+docs/
+ forsattsblad-ocr.md OCR-kontraktet för försättsbladet (Fas 0)
+tools/
+ README.md buntad SumatraPDF (checkas ej in)
+ Remiss.CoverPreview/ dev-verktyg: rendera försättsblad för öga-test
src/
Remiss.Core/ all logik, UI-oberoende
Abstractions/ IMottagningRepository, IPrinter
Models/ Mottagning
Repositories/ JsonMottagningRepository
- Pdf/ CoverPageGenerator (MigraDoc), PdfMerger (PdfSharp)
+ Pdf/ CoverPageGenerator, PdfMerger, OcrContract, WindowsFontResolver
Printing/ SumatraPdfPrinter (retry)
Security/ SecureTempStore (ACL + shred), SecureFile
Audit/ AuditLogger (JSONL + separat error-logg)
@@ -99,7 +111,8 @@ Remiss-PDF:erna är känsliga personuppgifter.
## Kända begränsningar (Fas 1)
-- Försättsbladets layout är preliminär tills Fas 0 är löst.
+- Försättsbladets layout följer OCR-kontrakt v1. Den bör verifieras mot
+ nedströmssystemets faktiska OCR innan pilot – ingen checksumma ännu.
- Mottagningsregistret är statisk JSON; HSA-katalog kopplas i Fas 3 bakom
`IMottagningRepository`.
- Tester täcker Core (PDF, register, temp). Dialog + utskrift är inte
diff --git a/Remiss.sln b/Remiss.sln
index 96df4fb..473e890 100644
--- a/Remiss.sln
+++ b/Remiss.sln
@@ -10,6 +10,10 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "tests", "tests", "{98039F5D
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Remiss.Core.Tests", "tests\Remiss.Core.Tests\Remiss.Core.Tests.csproj", "{8CE7B5D2-E03B-44A3-9D0B-CF114500A874}"
EndProject
+Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "tools", "tools", "{B883032E-2606-471A-9495-DB672695790D}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Remiss.CoverPreview", "tools\Remiss.CoverPreview\Remiss.CoverPreview.csproj", "{FAC4DDCA-F1A4-4AA1-B8D5-BBF934E78288}"
+EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@@ -28,11 +32,16 @@ Global
{8CE7B5D2-E03B-44A3-9D0B-CF114500A874}.Debug|Any CPU.Build.0 = Debug|Any CPU
{8CE7B5D2-E03B-44A3-9D0B-CF114500A874}.Release|Any CPU.ActiveCfg = Release|Any CPU
{8CE7B5D2-E03B-44A3-9D0B-CF114500A874}.Release|Any CPU.Build.0 = Release|Any CPU
+ {FAC4DDCA-F1A4-4AA1-B8D5-BBF934E78288}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {FAC4DDCA-F1A4-4AA1-B8D5-BBF934E78288}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {FAC4DDCA-F1A4-4AA1-B8D5-BBF934E78288}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {FAC4DDCA-F1A4-4AA1-B8D5-BBF934E78288}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
EndGlobalSection
GlobalSection(NestedProjects) = preSolution
{8CE7B5D2-E03B-44A3-9D0B-CF114500A874} = {98039F5D-2EEF-48E3-9B66-B40FE8971177}
+ {FAC4DDCA-F1A4-4AA1-B8D5-BBF934E78288} = {B883032E-2606-471A-9495-DB672695790D}
EndGlobalSection
EndGlobal
diff --git a/docs/forsattsblad-ocr.md b/docs/forsattsblad-ocr.md
new file mode 100644
index 0000000..5e902a8
--- /dev/null
+++ b/docs/forsattsblad-ocr.md
@@ -0,0 +1,46 @@
+# Försättsbladets OCR-kontrakt (v1)
+
+Fas 0-beslutet: nedströmssystemet läser mottagningen från försättsbladet med
+**server-side OCR på text** (ingen streckkod, ingen metadatafil). Vi äger formatet.
+
+## Vad som renderas på sida 1
+
+| Element | Syfte | Formatering |
+|---------|-------|-------------|
+| `REMISS` | Ankare: "det här är ett remiss-försättsblad" | Arial, 48 pt, fet |
+| `HSA-ID: <id>` | **Maskinläsbar rad** – den enda parsern behöver | Consolas (monospace), 20 pt, fet, egen rad, ≥0,8 cm luft över och under |
+| `Mottagning: <namn>` | Människoläsbar kontroll | Arial, 14 pt |
+| `Remiss bifogad som sida 2.` | Instruktion till mottagaren | Arial, 16 pt, fet |
+| Genereringstidpunkt | Spårbarhet | Arial, 9 pt |
+
+Allt vänsterställt, överst på A4, svart på vitt. Ingen patientdata på bladet.
+
+## Parserkontrakt
+
+Kontraktet är kod, inte bara text: [`OcrContract`](../src/Remiss.Core/Pdf/OcrContract.cs).
+
+- **Skriva:** `OcrContract.FormatHsaIdLine(hsaId)` → `"HSA-ID: SE2321000131-E000000000001"`
+- **Läsa:** `OcrContract.TryParseHsaId(ocrText, out var hsaId)` körs på OCR-texten
+ från sida 1.
+
+Parsern tål vanligt OCR-brus:
+
+- versaler/gemener spelar ingen roll (`HSA-ID`, `hsa id`)
+- tappat kolon eller bindestreck i etiketten
+- enstaka blanksteg som OCR:n lagt in mitt i id:t (`SE2321000131 - E000…`)
+- omgivande text och radbrytningar före/efter
+
+Den stannar vid radslut, så den läcker inte in i `Mottagning:`-raden under.
+
+## Inte med i v1
+
+- **Kontrollsiffra / checksumma.** Medvetet utelämnad nu. Läggs till i Fas 3 om
+ pilottestet visar att OCR:n läser fel – då kan servern validera i stället för
+ att skicka remissen till fel mottagning.
+- Fast geometrisk OCR-zon. Vi ankrar på nyckelordet `HSA-ID:`, inte på koordinater.
+
+## Iterera på layouten
+
+```powershell
+dotnet run --project tools/Remiss.CoverPreview -- preview.pdf SE2321000131-E000000000009 "Testmottagning"
+```
diff --git a/src/Remiss.Core/Pdf/CoverPageGenerator.cs b/src/Remiss.Core/Pdf/CoverPageGenerator.cs
index 98e2bce..5b9c4e6 100644
--- a/src/Remiss.Core/Pdf/CoverPageGenerator.cs
+++ b/src/Remiss.Core/Pdf/CoverPageGenerator.cs
@@ -1,5 +1,6 @@
using MigraDoc.DocumentObjectModel;
using MigraDoc.Rendering;
+using PdfSharp.Fonts;
namespace Remiss.Core.Pdf;
@@ -7,46 +8,69 @@ namespace Remiss.Core.Pdf;
public sealed record CoverInfo(string MottagningNamn, string HsaId, DateTime Skapad);
/// <summary>
-/// Genererar försättsbladet (sida 1). Layouten är preliminär – den spikas först
-/// när Fas 0 (hur nedströms-skrivaren läser av mottagning) är löst.
+/// Genererar försättsbladet (sida 1). Layouten följer <see cref="OcrContract"/>:
+/// ankarordet REMISS överst, därefter den maskinläsbara HSA-ID-raden i monospace
+/// på egen rad med gott om luft omkring, så att server-OCR:n träffar rätt.
/// </summary>
public static class CoverPageGenerator
{
+ // Consolas finns på alla Windows-installationer sedan Vista. Byt till
+ // "Courier New" om målmiljön saknar den.
+ private const string MonospaceFont = "Consolas";
+ private const string SansFont = "Arial";
+
+ static CoverPageGenerator()
+ {
+ // PdfSharp 6.1 kräver en explicit resolver för namngivna typsnitt.
+ GlobalFontSettings.FontResolver ??= new WindowsFontResolver();
+ }
+
public static void Generate(string outputPath, CoverInfo info)
{
- // PdfSharp 6.1 har en inbyggd font-resolver för Windows – ingen manuell
- // GlobalFontSettings.FontResolver behövs så länge appen är Windows-only.
var doc = new Document();
doc.Info.Title = "Remiss – försättsblad";
+ var normal = doc.Styles["Normal"]!;
+ normal.Font.Name = SansFont;
+ normal.Font.Size = 12;
+ normal.Font.Color = Colors.Black;
+
var section = doc.AddSection();
section.PageSetup.PageFormat = PageFormat.A4;
- section.PageSetup.TopMargin = Unit.FromCentimeter(3);
+ section.PageSetup.TopMargin = Unit.FromCentimeter(2.5);
section.PageSetup.LeftMargin = Unit.FromCentimeter(2.5);
section.PageSetup.RightMargin = Unit.FromCentimeter(2.5);
- var heading = section.AddParagraph("REMISS");
- heading.Format.Font.Size = 42;
+ // Ankare: dokumenttyp, stort och fetstilt.
+ var heading = section.AddParagraph(OcrContract.DocumentMarker);
+ heading.Format.Font.Name = SansFont;
+ heading.Format.Font.Size = 48;
heading.Format.Font.Bold = true;
- heading.Format.SpaceAfter = Unit.FromCentimeter(1.5);
-
- var to = section.AddParagraph();
- to.Format.Font.Size = 16;
- to.Format.SpaceAfter = Unit.FromCentimeter(2);
- to.AddText("Till mottagning:");
- to.AddLineBreak();
- var name = to.AddFormattedText(info.MottagningNamn, TextFormat.Bold);
- name.Font.Size = 22;
- to.AddLineBreak();
- to.AddText($"HSA-id: {info.HsaId}");
+ heading.Format.SpaceAfter = Unit.FromCentimeter(1.4);
+
+ // Maskinläsbar rad: egen rad, monospace, stor, svart på vitt, mycket luft.
+ var machine = section.AddParagraph(OcrContract.FormatHsaIdLine(info.HsaId));
+ machine.Format.Font.Name = MonospaceFont;
+ machine.Format.Font.Size = 20;
+ machine.Format.Font.Bold = true;
+ machine.Format.SpaceBefore = Unit.FromCentimeter(0.8);
+ machine.Format.SpaceAfter = Unit.FromCentimeter(0.8);
+
+ // Människoläsbar kontext – används inte av parsern.
+ var human = section.AddParagraph();
+ human.Format.Font.Size = 14;
+ human.Format.SpaceBefore = Unit.FromCentimeter(1.2);
+ human.AddText("Mottagning: ");
+ human.AddFormattedText(info.MottagningNamn, TextFormat.Bold);
var note = section.AddParagraph("Remiss bifogad som sida 2.");
- note.Format.Font.Size = 20;
+ note.Format.Font.Size = 16;
note.Format.Font.Bold = true;
+ note.Format.SpaceBefore = Unit.FromCentimeter(1.5);
var foot = section.AddParagraph($"Försättsblad genererat {info.Skapad:yyyy-MM-dd HH:mm}");
foot.Format.Font.Size = 9;
- foot.Format.SpaceBefore = Unit.FromCentimeter(3);
+ foot.Format.SpaceBefore = Unit.FromCentimeter(4);
var renderer = new PdfDocumentRenderer { Document = doc };
renderer.RenderDocument();
diff --git a/src/Remiss.Core/Pdf/OcrContract.cs b/src/Remiss.Core/Pdf/OcrContract.cs
new file mode 100644
index 0000000..6328b4f
--- /dev/null
+++ b/src/Remiss.Core/Pdf/OcrContract.cs
@@ -0,0 +1,49 @@
+using System.Text.RegularExpressions;
+
+namespace Remiss.Core.Pdf;
+
+/// <summary>
+/// Kontrakt (v1) för det maskinläsbara försättsbladet. Vi äger formatet.
+///
+/// Sida 1 i den sammanfogade PDF:en innehåller:
+/// • ordet <see cref="DocumentMarker"/> stort – ankare för "det här är en remiss"
+/// • en egen rad enligt <see cref="FormatHsaIdLine"/>: nyckelordet
+/// <see cref="HsaIdLabel"/> följt av HSA-id:t, i monospace, överst till vänster
+///
+/// Nedströms server-OCR läser sida 1 och kör <see cref="TryParseHsaId"/> på texten.
+/// Parsern är avsiktligt tolerant mot vanliga OCR-fel (tappat bindestreck/kolon,
+/// blandade versaler/gemener, extra blanksteg).
+/// </summary>
+public static class OcrContract
+{
+ public const string DocumentMarker = "REMISS";
+ public const string HsaIdLabel = "HSA-ID:";
+
+ /// <summary>Exakt den rad som ska renderas maskinläsbart på försättsbladet.</summary>
+ public static string FormatHsaIdLine(string hsaId) => $"{HsaIdLabel} {hsaId.Trim()}";
+
+ // Etiketten följt av id:t på samma rad. Tål tappat bindestreck/kolon och
+ // enstaka blanksteg som OCR lagt in i id:t – men inte radbrytningar, så
+ // matchningen inte rinner vidare in i människoläsbar text under.
+ private static readonly Regex HsaIdPattern = new(
+ @"HSA[-\s]?ID[:\s]*([A-Za-z0-9][A-Za-z0-9 \-]{2,}[A-Za-z0-9])",
+ RegexOptions.IgnoreCase | RegexOptions.CultureInvariant | RegexOptions.Compiled);
+
+ /// <summary>
+ /// Plockar ut HSA-id ur (ev. brusig) OCR-text från sida 1.
+ /// Blanksteg som OCR:n råkat lägga in mitt i id:t tas bort.
+ /// </summary>
+ public static bool TryParseHsaId(string ocrText, out string hsaId)
+ {
+ hsaId = "";
+ if (string.IsNullOrWhiteSpace(ocrText))
+ return false;
+
+ var match = HsaIdPattern.Match(ocrText);
+ if (!match.Success)
+ return false;
+
+ hsaId = Regex.Replace(match.Groups[1].Value, @"\s+", "");
+ return hsaId.Length > 0;
+ }
+}
diff --git a/src/Remiss.Core/Pdf/WindowsFontResolver.cs b/src/Remiss.Core/Pdf/WindowsFontResolver.cs
new file mode 100644
index 0000000..3010970
--- /dev/null
+++ b/src/Remiss.Core/Pdf/WindowsFontResolver.cs
@@ -0,0 +1,38 @@
+using PdfSharp.Fonts;
+
+namespace Remiss.Core.Pdf;
+
+/// <summary>
+/// Minimal font-resolver för Windows. PdfSharp 6.1 resolvar inte namngivna
+/// typsnitt automatiskt – vi pekar ut TTF-filerna i C:\Windows\Fonts själva.
+/// Appen är Windows-only, så det räcker gott.
+///
+/// Endast de typsnitt försättsbladet använder stöds; allt annat faller tillbaka
+/// på Arial. (Remissen sida 2 renderas aldrig av oss – den importeras bara.)
+/// </summary>
+internal sealed class WindowsFontResolver : IFontResolver
+{
+ private static readonly string FontDir = Environment.GetFolderPath(Environment.SpecialFolder.Fonts);
+ private readonly Dictionary<string, byte[]> _cache = new(StringComparer.OrdinalIgnoreCase);
+
+ public FontResolverInfo ResolveTypeface(string familyName, bool isBold, bool isItalic)
+ {
+ var isConsolas = familyName.Trim().Equals("Consolas", StringComparison.OrdinalIgnoreCase);
+
+ var face = isConsolas
+ ? "consola" + (isBold, isItalic) switch { (true, true) => "z", (true, false) => "b", (false, true) => "i", _ => "" }
+ : "arial" + (isBold, isItalic) switch { (true, true) => "bi", (true, false) => "bd", (false, true) => "i", _ => "" };
+
+ return new FontResolverInfo(face);
+ }
+
+ public byte[] GetFont(string faceName)
+ {
+ if (_cache.TryGetValue(faceName, out var cached))
+ return cached;
+
+ var bytes = File.ReadAllBytes(Path.Combine(FontDir, faceName + ".ttf"));
+ _cache[faceName] = bytes;
+ return bytes;
+ }
+}
diff --git a/tests/Remiss.Core.Tests/OcrContractTests.cs b/tests/Remiss.Core.Tests/OcrContractTests.cs
new file mode 100644
index 0000000..8a4f3b6
--- /dev/null
+++ b/tests/Remiss.Core.Tests/OcrContractTests.cs
@@ -0,0 +1,42 @@
+using Remiss.Core.Pdf;
+using Xunit;
+
+namespace Remiss.Core.Tests;
+
+public sealed class OcrContractTests
+{
+ private const string Id = "SE2321000131-E000000000001";
+
+ [Fact]
+ public void FormatHsaIdLine_usesTheAgreedLabel()
+ {
+ Assert.Equal("HSA-ID: " + Id, OcrContract.FormatHsaIdLine(" " + Id + " "));
+ }
+
+ [Fact]
+ public void TryParseHsaId_roundTripsTheRenderedLine()
+ {
+ Assert.True(OcrContract.TryParseHsaId(OcrContract.FormatHsaIdLine(Id), out var parsed));
+ Assert.Equal(Id, parsed);
+ }
+
+ [Theory]
+ [InlineData("REMISS\nHSA-ID: SE2321000131-E000000000001\nMottagning: Hud SU", "SE2321000131-E000000000001")]
+ [InlineData("hsa id SE2321000131-E000000000001", "SE2321000131-E000000000001")] // tappat kolon + bindestreck
+ [InlineData("HSA-ID: SE2321000131 - E000000000001", "SE2321000131-E000000000001")] // OCR-blanksteg i id:t
+ [InlineData("brus...\r\n HSA-ID:SE2321000131-E000000000001 \r\nmer brus", "SE2321000131-E000000000001")]
+ public void TryParseHsaId_toleratesCommonOcrNoise(string ocrText, string expected)
+ {
+ Assert.True(OcrContract.TryParseHsaId(ocrText, out var parsed));
+ Assert.Equal(expected, parsed);
+ }
+
+ [Theory]
+ [InlineData("")]
+ [InlineData("En helt vanlig textrad utan id")]
+ [InlineData("HSA-ID:")]
+ public void TryParseHsaId_returnsFalseWhenNothingToFind(string ocrText)
+ {
+ Assert.False(OcrContract.TryParseHsaId(ocrText, out _));
+ }
+}
diff --git a/tools/Remiss.CoverPreview/Program.cs b/tools/Remiss.CoverPreview/Program.cs
new file mode 100644
index 0000000..5526f21
--- /dev/null
+++ b/tools/Remiss.CoverPreview/Program.cs
@@ -0,0 +1,19 @@
+using Remiss.Core.Pdf;
+
+// Dev-verktyg: renderar ett försättsblad så man kan öga-testa OCR-layouten
+// utan hela utskriftsflödet.
+//
+// dotnet run --project tools/Remiss.CoverPreview -- [utfil.pdf] [HSA-id] [mottagningsnamn]
+
+var outPath = args.ElementAtOrDefault(0) ?? Path.Combine(Environment.CurrentDirectory, "forsattsblad-preview.pdf");
+var hsaId = args.ElementAtOrDefault(1) ?? "SE2321000131-E000000000001";
+var namn = args.ElementAtOrDefault(2) ?? "Hudmottagningen Sahlgrenska";
+
+CoverPageGenerator.Generate(outPath, new CoverInfo(namn, hsaId, DateTime.Now));
+
+Console.WriteLine($"Skrev {outPath}");
+Console.WriteLine($"Maskinläsbar rad: {OcrContract.FormatHsaIdLine(hsaId)}");
+
+// Sanity: läs tillbaka som om det vore OCR-text.
+if (OcrContract.TryParseHsaId(OcrContract.FormatHsaIdLine(hsaId), out var parsed))
+ Console.WriteLine($"Parser läser tillbaka: {parsed} ({(parsed == hsaId ? "OK" : "AVVIKER")})");
diff --git a/tools/Remiss.CoverPreview/Remiss.CoverPreview.csproj b/tools/Remiss.CoverPreview/Remiss.CoverPreview.csproj
new file mode 100644
index 0000000..45510d4
--- /dev/null
+++ b/tools/Remiss.CoverPreview/Remiss.CoverPreview.csproj
@@ -0,0 +1,15 @@
+<Project Sdk="Microsoft.NET.Sdk">
+
+ <PropertyGroup>
+ <OutputType>Exe</OutputType>
+ <TargetFramework>net8.0-windows</TargetFramework>
+ <Nullable>enable</Nullable>
+ <ImplicitUsings>enable</ImplicitUsings>
+ <RootNamespace>Remiss.CoverPreview</RootNamespace>
+ </PropertyGroup>
+
+ <ItemGroup>
+ <ProjectReference Include="..\..\src\Remiss.Core\Remiss.Core.csproj" />
+ </ItemGroup>
+
+</Project>