Skip to content
Open
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
Original file line number Diff line number Diff line change
Expand Up @@ -293,10 +293,14 @@ public static ParameterCollection Parse(string data)
var key = entry.Substring(0, equalsIndex);
var value = equalsIndex < entry.Length - 1 ? entry.Substring(equalsIndex + 1) : """";

// Handle UTF-8 prefix
// Handle UTF-8 prefix: the surrounding block was decoded as Windows-1252, so a
// %UTF8%-keyed value arrives one char per raw byte (mojibake). Decode it here so
// the UTF-8 variant — which Altium writes before its plain-ANSI twin, and which
// the first-match indexer therefore returns — carries the correct string.
if (key.StartsWith(""%UTF8%"", StringComparison.OrdinalIgnoreCase))
{
key = key.Substring(6);
value = global::OriginalCircuit.Altium.Serialization.AltiumEncoding.DecodeUtf8ParameterValue(value);
}

result._parameters.Add(new KeyValuePair<string, string>(key, value.TrimEnd('\r', '\n')));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -751,7 +751,7 @@ public void RenderTextFrame(IRenderContext context, SchTextFrame textFrame)

if (!string.IsNullOrEmpty(textFrame.Text))
{
var frameText = FixTextEncoding(textFrame.Text);
var frameText = textFrame.Text;
var textColor = ColorHelper.BgrToArgb(textFrame.TextColor);
var font = GetFont(textFrame.FontId);
var fontSize = GetFontSize(textFrame.FontId);
Expand Down Expand Up @@ -963,7 +963,7 @@ public void RenderJunction(IRenderContext context, SchJunction junction)
public void RenderNetLabel(IRenderContext context, SchNetLabel netLabel)
{
if (string.IsNullOrEmpty(netLabel.Text)) return;
var netText = FixTextEncoding(netLabel.Text);
var netText = netLabel.Text;

var (sx, sy) = _transform.WorldToScreen(netLabel.Location.X, netLabel.Location.Y);
var color = GetArgbColor(netLabel.Color);
Expand Down Expand Up @@ -1808,7 +1808,7 @@ public void RenderHarnessConnector(IRenderContext context, SchHarnessConnector c
var font = GetFont(tl.FontId);
// Altium anchors the harness-type label at the RIGHT of the text (the label's Location is
// its right edge), so the name extends leftward from there.
context.DrawText(FixTextEncoding(tl.Text), tx, ty, GetFontSize(tl.FontId), tcolor,
context.DrawText(tl.Text, tx, ty, GetFontSize(tl.FontId), tcolor,
new TextRenderOptions
{
FontFamily = font.FontName,
Expand Down Expand Up @@ -1844,7 +1844,7 @@ private void RenderHarnessEntry(IRenderContext context, SchHarnessConnector conn
// members inside the connector box).
double margin = _transform.ScaleValue(Coord.FromMils(15));
double textX = right ? boxX + boxW - margin : boxX + margin;
context.DrawText(FixTextEncoding(entry.Text), textX, py, fontSize, textColor,
context.DrawText(entry.Text, textX, py, fontSize, textColor,
new TextRenderOptions
{
FontFamily = font.FontName,
Expand Down Expand Up @@ -2003,7 +2003,7 @@ private void RenderOverlines(IRenderContext context, List<OverlineHelper.TextSeg
/// parameter value from the current component's parameter list.
/// For example, "=Value" resolves to the Value parameter's text.
/// </summary>
private string ResolveStringIndirection(string text) => FixTextEncoding(Resolve(text));
private string ResolveStringIndirection(string text) => Resolve(text);

private string Resolve(string text)
{
Expand Down Expand Up @@ -2072,9 +2072,10 @@ private bool TryResolveComputedString(string name, out string value)
}

/// <summary>
/// Repairs UTF-8 parameter values that were decoded as Windows-1252 (Altium stores some text,
/// e.g. "µF", UTF-8-encoded behind a %UTF8% marker the reader doesn't decode, so "µ" arrives as
/// "µ"). Re-interprets the Latin-1 bytes as UTF-8 when that yields a valid string.
/// Repairs UTF-8 text that was decoded as Windows-1252, so "µ" arrives as "µ". Parameter-string
/// values are decoded by ParameterCollection.Parse (%UTF8% keys), so this remains only for pin
/// names, which come from binary pin records with no encoding marker. Re-interprets the Latin-1
/// bytes as UTF-8 when that yields a valid string.
/// </summary>
internal static string FixTextEncoding(string text)
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1054,9 +1054,10 @@ private static SchJunction CreateJunction(ParameterCollection p)
private static SchParameter CreateParameter(ParameterCollection p, Dictionary<string, string>? rawParameters = null)
{
var dto = Dto.Sch.SchParameterDto.FromParameters(p);
// ParameterCollection.Parse already decoded a %UTF8%-keyed Text value; the raw dictionary
// keeps the prefixed key, so detect it here only to preserve the prefix on write.
var isUtf8 = rawParameters?.ContainsKey("%UTF8%TEXT") == true;
var value = dto.Text ?? string.Empty;
if (isUtf8) value = AltiumEncoding.DecodeUtf8ParameterValue(value);
return new SchParameter
{
Name = dto.Name ?? string.Empty,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1675,11 +1675,10 @@ private static SchParameter CreateParameter(Dictionary<string, string> parameter
var paramCollection = ToParameterCollection(parameters);
var dto = SchParameterDto.FromParameters(paramCollection);

// The Text value is UTF-8 when its key carried the %UTF8% prefix; the dictionary keeps the
// raw key, so detect it here and decode the (otherwise Windows-1252-mis-decoded) value.
// ParameterCollection.Parse already decoded a %UTF8%-keyed Text value; the dictionary keeps
// the raw key, so detect the prefix here only to preserve it on write (TextIsUtf8).
var isUtf8 = parameters.ContainsKey("%UTF8%Text");
var value = dto.Text ?? string.Empty;
if (isUtf8) value = AltiumEncoding.DecodeUtf8ParameterValue(value);

return new SchParameter
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -368,9 +368,14 @@ internal static void WriteComponentRecord(BinaryFormatWriter writer, SchComponen
};
// Altium omits ComponentDescription entirely when a component has no description. Writing an
// empty value perturbs the byte-faithful record and reads back as "" instead of null, so only
// emit it (in its original position) when a description is actually present.
// emit it (in its original position) when a description is actually present. A description
// Windows-1252 cannot represent is promoted to a %UTF8% parameter, as with SchParameter.Text.
if (component.Description != null)
parameters["ComponentDescription"] = component.Description;
{
var descUtf8 = RequiresUtf8(component.Description);
parameters[descUtf8 ? "%UTF8%ComponentDescription" : "ComponentDescription"] =
descUtf8 ? AltiumEncoding.EncodeUtf8ParameterValue(component.Description) : component.Description;
}
parameters["PartCount"] = (component.PartCount + 1).ToString(CultureInfo.InvariantCulture);
parameters["DisplayModeCount"] = "1";
parameters["IndexInSheet"] = "-1";
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -51,4 +51,35 @@ public void SchDoc_Utf8FlaggedParameter_RoundTripsValueAndFlag()
Assert.Equal(text, param.Value);
Assert.True(param.TextIsUtf8);
}

/// <summary>
/// Altium writes non-ASCII values twice: a %UTF8% variant first, then a plain-ANSI twin
/// (e.g. <c>|%UTF8%ComponentDescription=KON 4.7µF|ComponentDescription=KON 4.7µF|</c>).
/// Both collapse onto one key when the prefix is stripped; the UTF-8 variant must win and be
/// decoded, not returned as mojibake (bd-23s).
/// </summary>
[Fact]
public void ParameterCollection_Utf8AndAnsiDuplicate_ReturnsDecodedValue()
{
var collection = OriginalCircuit.Altium.Primitives.ParameterCollection.Parse(
"|RECORD=1|%UTF8%ComponentDescription=KON 4.7µF|ComponentDescription=KON 4.7µF|PartCount=2");

Assert.Equal("KON 4.7µF", collection["ComponentDescription"].AsStringOrDefault());
}

[Theory]
[InlineData("KON 4.7µF ±10%")] // Windows-1252 representable → plain ComponentDescription key
[InlineData("Shunt 5mΩ")] // Ω is not 1252-representable → %UTF8%ComponentDescription key
public void SchDoc_NonAsciiComponentDescription_RoundTrips(string description)
{
var doc = new SchDocument();
doc.AddComponent(new SchComponent { Name = "R1", PartCount = 1, Description = description });

using var ms = new MemoryStream();
new SchDocWriter().Write(doc, ms);
ms.Position = 0;
var readBack = new SchDocReader().Read(ms);

Assert.Equal(description, readBack.Components.First().Description);
}
}