mirror of
https://github.com/UglyToad/PdfPig.git
synced 2025-10-15 19:54:52 +08:00
#6 complete parsing of type 2 charstrings. subroutines now eager evaluated from raw bytes
This commit is contained in:
@@ -2,7 +2,6 @@
|
|||||||
{
|
{
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using Geometry;
|
using Geometry;
|
||||||
using Util;
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// The context used and updated when interpreting the commands for a charstring.
|
/// The context used and updated when interpreting the commands for a charstring.
|
||||||
@@ -10,17 +9,7 @@
|
|||||||
internal class Type2BuildCharContext
|
internal class Type2BuildCharContext
|
||||||
{
|
{
|
||||||
private readonly Dictionary<int, decimal> transientArray = new Dictionary<int, decimal>();
|
private readonly Dictionary<int, decimal> transientArray = new Dictionary<int, decimal>();
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// The local subroutines available in this font.
|
|
||||||
/// </summary>
|
|
||||||
public IReadOnlyDictionary<int, Type2CharStrings.CommandSequence> LocalSubroutines { get; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// The global subroutines available in this font set.
|
|
||||||
/// </summary>
|
|
||||||
public IReadOnlyDictionary<int, Type2CharStrings.CommandSequence> GlobalSubroutines { get; }
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// The numbers currently on the Type 2 Build Char stack.
|
/// The numbers currently on the Type 2 Build Char stack.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -42,18 +31,6 @@
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public decimal? Width { get; set; }
|
public decimal? Width { get; set; }
|
||||||
|
|
||||||
public List<Union<decimal, LazyType2Command>> All { get; } = new List<Union<decimal, LazyType2Command>>();
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Create a new <see cref="Type2BuildCharContext"/>.
|
|
||||||
/// </summary>
|
|
||||||
public Type2BuildCharContext(IReadOnlyDictionary<int, Type2CharStrings.CommandSequence> localSubroutines,
|
|
||||||
IReadOnlyDictionary<int, Type2CharStrings.CommandSequence> globalSubroutines)
|
|
||||||
{
|
|
||||||
LocalSubroutines = localSubroutines;
|
|
||||||
GlobalSubroutines = globalSubroutines;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void AddRelativeHorizontalLine(decimal dx)
|
public void AddRelativeHorizontalLine(decimal dx)
|
||||||
{
|
{
|
||||||
AddRelativeLine(dx, 0);
|
AddRelativeLine(dx, 0);
|
||||||
@@ -107,18 +84,6 @@
|
|||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
public int GetLocalSubroutineBias()
|
|
||||||
{
|
|
||||||
var count = LocalSubroutines.Count;
|
|
||||||
return CountToBias(count);
|
|
||||||
}
|
|
||||||
|
|
||||||
public int GetGlobalSubroutineBias()
|
|
||||||
{
|
|
||||||
var count = GlobalSubroutines.Count;
|
|
||||||
return CountToBias(count);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static int CountToBias(int count)
|
public static int CountToBias(int count)
|
||||||
{
|
{
|
||||||
if (count < 1240)
|
if (count < 1240)
|
||||||
@@ -133,15 +98,5 @@
|
|||||||
|
|
||||||
return 32768;
|
return 32768;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void EvaluateSubroutine(Type2CharStrings.CommandSequence subroutine)
|
|
||||||
{
|
|
||||||
foreach (var command in subroutine.Commands)
|
|
||||||
{
|
|
||||||
All.Add(command);
|
|
||||||
command.Match(x => Stack.Push(x),
|
|
||||||
act => act.Run(this));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
@@ -190,15 +190,8 @@
|
|||||||
ctx.Stack.Clear();
|
ctx.Stack.Clear();
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
{ 10, new LazyType2Command("callsubr", ctx =>
|
{ 10, new LazyType2Command("callsubr", ctx => {})},
|
||||||
{
|
{ 11, new LazyType2Command("return", ctx => {})},
|
||||||
var index = (int)ctx.Stack.PopTop();
|
|
||||||
var bias = ctx.GetLocalSubroutineBias();
|
|
||||||
var actualIndex = index + bias;
|
|
||||||
var subr = ctx.LocalSubroutines[actualIndex];
|
|
||||||
ctx.EvaluateSubroutine(subr);
|
|
||||||
})},
|
|
||||||
{ 11, new LazyType2Command("return", x => { })},
|
|
||||||
{ 14, new LazyType2Command("endchar", ctx =>
|
{ 14, new LazyType2Command("endchar", ctx =>
|
||||||
{
|
{
|
||||||
ctx.Stack.Clear();
|
ctx.Stack.Clear();
|
||||||
@@ -387,14 +380,7 @@
|
|||||||
ctx.Stack.Clear();
|
ctx.Stack.Clear();
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
{ 29, new LazyType2Command("callgsubr", ctx =>
|
{ 29, new LazyType2Command("callgsubr", ctx => {})
|
||||||
{
|
|
||||||
var index = (int)ctx.Stack.PopTop();
|
|
||||||
var bias = ctx.GetGlobalSubroutineBias();
|
|
||||||
var actualIndex = index + bias;
|
|
||||||
var subr = ctx.GlobalSubroutines[actualIndex];
|
|
||||||
ctx.EvaluateSubroutine(subr);
|
|
||||||
})
|
|
||||||
},
|
},
|
||||||
{ 30,
|
{ 30,
|
||||||
new LazyType2Command("vhcurveto", ctx =>
|
new LazyType2Command("vhcurveto", ctx =>
|
||||||
@@ -711,38 +697,22 @@
|
|||||||
{
|
{
|
||||||
throw new ArgumentNullException(nameof(globalSubroutines));
|
throw new ArgumentNullException(nameof(globalSubroutines));
|
||||||
}
|
}
|
||||||
|
|
||||||
var globalSubroutineSequences = new Dictionary<int, Type2CharStrings.CommandSequence>();
|
|
||||||
for (var i = 0; i < globalSubroutines.Count; i++)
|
|
||||||
{
|
|
||||||
var bytes = globalSubroutines[i];
|
|
||||||
var sequence = ParseSingle(bytes, null, null);
|
|
||||||
globalSubroutineSequences[i] = new Type2CharStrings.CommandSequence(sequence);
|
|
||||||
}
|
|
||||||
|
|
||||||
var localSubroutineSequences = new Dictionary<int, Type2CharStrings.CommandSequence>();
|
|
||||||
for (var i = 0; i < localSubroutines.Count; i++)
|
|
||||||
{
|
|
||||||
var bytes = localSubroutines[i];
|
|
||||||
var sequence = ParseSingle(bytes, null, globalSubroutineSequences);
|
|
||||||
localSubroutineSequences[i] = new Type2CharStrings.CommandSequence(sequence);
|
|
||||||
}
|
|
||||||
|
|
||||||
var charStrings = new Dictionary<string, Type2CharStrings.CommandSequence>();
|
var charStrings = new Dictionary<string, Type2CharStrings.CommandSequence>();
|
||||||
for (var i = 0; i < charStringBytes.Count; i++)
|
for (var i = 0; i < charStringBytes.Count; i++)
|
||||||
{
|
{
|
||||||
var charString = charStringBytes[i];
|
var charString = charStringBytes[i];
|
||||||
var name = charset.GetNameByGlyphId(i);
|
var name = charset.GetNameByGlyphId(i);
|
||||||
var sequence = ParseSingle(charString, localSubroutineSequences, globalSubroutineSequences);
|
var sequence = ParseSingle(charString.ToList(), localSubroutines, globalSubroutines);
|
||||||
charStrings[name] = new Type2CharStrings.CommandSequence(sequence);
|
charStrings[name] = new Type2CharStrings.CommandSequence(sequence);
|
||||||
}
|
}
|
||||||
|
|
||||||
return new Type2CharStrings(charStrings, localSubroutineSequences, globalSubroutineSequences);
|
return new Type2CharStrings(charStrings, localSubroutines, globalSubroutines);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static IReadOnlyList<Union<decimal, LazyType2Command>> ParseSingle(IReadOnlyList<byte> bytes,
|
private static IReadOnlyList<Union<decimal, LazyType2Command>> ParseSingle(List<byte> bytes,
|
||||||
Dictionary<int, Type2CharStrings.CommandSequence> localSubroutines,
|
CompactFontFormatIndex localSubroutines,
|
||||||
Dictionary<int, Type2CharStrings.CommandSequence> globalSubroutines)
|
CompactFontFormatIndex globalSubroutines)
|
||||||
{
|
{
|
||||||
var instructions = new List<Union<decimal, LazyType2Command>>();
|
var instructions = new List<Union<decimal, LazyType2Command>>();
|
||||||
for (var i = 0; i < bytes.Count; i++)
|
for (var i = 0; i < bytes.Count; i++)
|
||||||
@@ -751,7 +721,10 @@
|
|||||||
if (b <= 31 && b != 28)
|
if (b <= 31 && b != 28)
|
||||||
{
|
{
|
||||||
var command = GetCommand(b, bytes, instructions, localSubroutines, globalSubroutines, ref i);
|
var command = GetCommand(b, bytes, instructions, localSubroutines, globalSubroutines, ref i);
|
||||||
instructions.Add(Union<decimal, LazyType2Command>.Two(command));
|
if (command != null)
|
||||||
|
{
|
||||||
|
instructions.Add(Union<decimal, LazyType2Command>.Two(command));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
@@ -803,10 +776,10 @@
|
|||||||
return lead + (fractionalPart / 65535m);
|
return lead + (fractionalPart / 65535m);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static LazyType2Command GetCommand(byte b, IReadOnlyList<byte> bytes,
|
private static LazyType2Command GetCommand(byte b, List<byte> bytes,
|
||||||
IReadOnlyList<Union<decimal, LazyType2Command>> precedingCommands,
|
List<Union<decimal, LazyType2Command>> precedingCommands,
|
||||||
Dictionary<int, Type2CharStrings.CommandSequence> localSubroutines,
|
CompactFontFormatIndex localSubroutines,
|
||||||
Dictionary<int, Type2CharStrings.CommandSequence> globalSubroutines, ref int i)
|
CompactFontFormatIndex globalSubroutines, ref int i)
|
||||||
{
|
{
|
||||||
if (b == 12)
|
if (b == 12)
|
||||||
{
|
{
|
||||||
@@ -819,28 +792,53 @@
|
|||||||
return new LazyType2Command($"unknown: {b} {b2}", x => { });
|
return new LazyType2Command($"unknown: {b} {b2}", x => { });
|
||||||
}
|
}
|
||||||
|
|
||||||
// hintmask and cntrmask
|
// Invoke a subroutine, substitute the subroutine bytes into this sequence.
|
||||||
if (b == 19 || b == 20)
|
if (b == 10 || b == 29)
|
||||||
{
|
{
|
||||||
var minimumFullBytes = CalculatePrecedingHintBytes(precedingCommands, localSubroutines, globalSubroutines);
|
var isLocal = b == 10;
|
||||||
|
int precedingNumber = 0;
|
||||||
|
precedingCommands[precedingCommands.Count - 1].Match(x => precedingNumber = (int)x,
|
||||||
|
_ => throw new InvalidOperationException("A subroutine call must be preceded by a number, not a command."));
|
||||||
|
|
||||||
|
var bias = Type2BuildCharContext.CountToBias(isLocal ? localSubroutines.Count : globalSubroutines.Count);
|
||||||
|
var index = precedingNumber + bias;
|
||||||
|
var subroutineBytes = isLocal ? localSubroutines[index] : globalSubroutines[index];
|
||||||
|
bytes.RemoveRange(i - 1, 2);
|
||||||
|
bytes.InsertRange(i - 1, subroutineBytes);
|
||||||
|
|
||||||
|
// Remove the subroutine index
|
||||||
|
precedingCommands.RemoveAt(precedingCommands.Count - 1);
|
||||||
|
i -= 2;
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (b == 19 || b == 20)
|
||||||
|
{
|
||||||
|
// hintmask and cntrmask
|
||||||
|
var minimumFullBytes = CalculatePrecedingHintBytes(precedingCommands);
|
||||||
// Skip the following hintmask or cntrmask data bytes
|
// Skip the following hintmask or cntrmask data bytes
|
||||||
i += minimumFullBytes;
|
i += minimumFullBytes;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (SingleByteCommandStore.TryGetValue(b, out var command))
|
if (SingleByteCommandStore.TryGetValue(b, out var command))
|
||||||
{
|
{
|
||||||
|
// Ignore return
|
||||||
|
if (command.Name == "return")
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
return command;
|
return command;
|
||||||
}
|
}
|
||||||
|
|
||||||
return new LazyType2Command($"unknown: {b}", x => { });
|
return new LazyType2Command($"unknown: {b}", x => { });
|
||||||
}
|
}
|
||||||
|
|
||||||
private static int CalculatePrecedingHintBytes(IReadOnlyList<Union<decimal, LazyType2Command>> precedingCommands,
|
private static int CalculatePrecedingHintBytes(IReadOnlyList<Union<decimal, LazyType2Command>> precedingCommands)
|
||||||
Dictionary<int, Type2CharStrings.CommandSequence> localSubroutines,
|
|
||||||
Dictionary<int, Type2CharStrings.CommandSequence> globalSubroutines)
|
|
||||||
{
|
{
|
||||||
int SafeStemCount(int counts)
|
int SafeStemCount(int counts)
|
||||||
{
|
{
|
||||||
|
// Where there an odd number of stem arguments take only as many as the even number requires.
|
||||||
if (counts % 2 == 0)
|
if (counts % 2 == 0)
|
||||||
{
|
{
|
||||||
return counts / 2;
|
return counts / 2;
|
||||||
@@ -848,8 +846,6 @@
|
|||||||
|
|
||||||
return (counts - 1) / 2;
|
return (counts - 1) / 2;
|
||||||
}
|
}
|
||||||
// Do a first pass to substitute in all local and global subroutines prior to the first hintmask.
|
|
||||||
var commandsToCountHints = BuildFullCommandSequence(precedingCommands, localSubroutines, globalSubroutines);
|
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* The hintmask operator is followed by one or more data bytes that specify the stem hints which are to be active for the
|
* The hintmask operator is followed by one or more data bytes that specify the stem hints which are to be active for the
|
||||||
@@ -860,9 +856,9 @@
|
|||||||
var stemCount = 0;
|
var stemCount = 0;
|
||||||
var precedingNumbers = 0;
|
var precedingNumbers = 0;
|
||||||
var hasEncounteredInitialHintMask = false;
|
var hasEncounteredInitialHintMask = false;
|
||||||
for (var j = 0; j < commandsToCountHints.Count; j++)
|
for (var j = 0; j < precedingCommands.Count; j++)
|
||||||
{
|
{
|
||||||
var item = commandsToCountHints[j];
|
var item = precedingCommands[j];
|
||||||
item.Match(x =>
|
item.Match(x =>
|
||||||
{
|
{
|
||||||
precedingNumbers++;
|
precedingNumbers++;
|
||||||
@@ -904,82 +900,5 @@
|
|||||||
|
|
||||||
return minimumFullBytes;
|
return minimumFullBytes;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static IReadOnlyList<Union<decimal, LazyType2Command>> BuildFullCommandSequence(IReadOnlyList<Union<decimal, LazyType2Command>> allNonSubroutineCommands,
|
|
||||||
Dictionary<int, Type2CharStrings.CommandSequence> localSubroutines,
|
|
||||||
Dictionary<int, Type2CharStrings.CommandSequence> globalSubroutines)
|
|
||||||
{
|
|
||||||
/*
|
|
||||||
* Since the initial hintmask or stem information may be in a subroutine we must include subroutines
|
|
||||||
* when calculating the overall number of hints. This is a nuisance because we have to evaluate the
|
|
||||||
* charstring but should be rare enough to avoid a performance hit.
|
|
||||||
*/
|
|
||||||
bool IsSubroutine(LazyType2Command cmd)
|
|
||||||
{
|
|
||||||
return cmd.Name == "callsubr" || cmd.Name == "callgsubr";
|
|
||||||
}
|
|
||||||
|
|
||||||
if (localSubroutines == null || globalSubroutines == null)
|
|
||||||
{
|
|
||||||
return allNonSubroutineCommands;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Build a mutable list of the commands to substitute subroutine into.
|
|
||||||
var results = new List<Union<decimal, LazyType2Command>>(allNonSubroutineCommands);
|
|
||||||
|
|
||||||
var firstHintmask = false;
|
|
||||||
|
|
||||||
var previousNumber = -1m;
|
|
||||||
for (var i = 0; i < results.Count; i++)
|
|
||||||
{
|
|
||||||
var command = results[i];
|
|
||||||
|
|
||||||
var foundSubroutine = false;
|
|
||||||
var wasLocalSubroutine = false;
|
|
||||||
var subroutineIndex = 0;
|
|
||||||
|
|
||||||
command.Match(x => previousNumber = x, x =>
|
|
||||||
{
|
|
||||||
// When we have a subroutine which appears before a hintmask we substitute the actual commands in to check for hints.
|
|
||||||
if (IsSubroutine(x) && !firstHintmask)
|
|
||||||
{
|
|
||||||
foundSubroutine = true;
|
|
||||||
wasLocalSubroutine = x.Name == "callsubr";
|
|
||||||
|
|
||||||
var bias = Type2BuildCharContext.CountToBias(wasLocalSubroutine ? localSubroutines.Count : globalSubroutines.Count);
|
|
||||||
subroutineIndex = (int) previousNumber + bias;
|
|
||||||
}
|
|
||||||
else if (x.Name == "hintmask")
|
|
||||||
{
|
|
||||||
firstHintmask = true;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
if (foundSubroutine)
|
|
||||||
{
|
|
||||||
// Replace the call to the local or global subroutine with the actual routine.
|
|
||||||
var routine = wasLocalSubroutine ? localSubroutines[subroutineIndex] : globalSubroutines[subroutineIndex];
|
|
||||||
results.RemoveRange(i-1, 2);
|
|
||||||
|
|
||||||
// Skip any return commands since they interfere with counting.
|
|
||||||
results.InsertRange(i-1 , routine.Commands.Where(x =>
|
|
||||||
{
|
|
||||||
var isReturn = false;
|
|
||||||
x.Match(_ => {}, y => isReturn = y.Name == "return");
|
|
||||||
return !isReturn;
|
|
||||||
}));
|
|
||||||
|
|
||||||
i -= 2;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Exit once we hit the first hintmask since all hints have now been declared.
|
|
||||||
if (firstHintmask)
|
|
||||||
{
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return results;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -4,6 +4,7 @@
|
|||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using Util;
|
using Util;
|
||||||
|
using Util.JetBrains.Annotations;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Stores the decoded command sequences for Type 2 CharStrings from a Compact Font Format font as well
|
/// Stores the decoded command sequences for Type 2 CharStrings from a Compact Font Format font as well
|
||||||
@@ -15,13 +16,25 @@
|
|||||||
private readonly object locker = new object();
|
private readonly object locker = new object();
|
||||||
private readonly Dictionary<string, CharacterPath> glyphs = new Dictionary<string, CharacterPath>();
|
private readonly Dictionary<string, CharacterPath> glyphs = new Dictionary<string, CharacterPath>();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The decoded charstrings in this font.
|
||||||
|
/// </summary>
|
||||||
public IReadOnlyDictionary<string, CommandSequence> CharStrings { get; }
|
public IReadOnlyDictionary<string, CommandSequence> CharStrings { get; }
|
||||||
|
|
||||||
public IReadOnlyDictionary<int, CommandSequence> LocalSubroutines { get; }
|
/// <summary>
|
||||||
public IReadOnlyDictionary<int, CommandSequence> GlobalSubroutines { get; }
|
/// The indexed bytes for the local subroutines in this font.
|
||||||
|
/// </summary>
|
||||||
|
[NotNull]
|
||||||
|
public CompactFontFormatIndex LocalSubroutines { get; }
|
||||||
|
|
||||||
public Type2CharStrings(IReadOnlyDictionary<string, CommandSequence> charStrings, IReadOnlyDictionary<int, CommandSequence> localSubroutines,
|
/// <summary>
|
||||||
IReadOnlyDictionary<int, CommandSequence> globalSubroutines)
|
/// The indexed bytes for the global subroutines in this font set.
|
||||||
|
/// </summary>
|
||||||
|
[NotNull]
|
||||||
|
public CompactFontFormatIndex GlobalSubroutines { get; }
|
||||||
|
|
||||||
|
public Type2CharStrings(IReadOnlyDictionary<string, CommandSequence> charStrings, CompactFontFormatIndex localSubroutines,
|
||||||
|
CompactFontFormatIndex globalSubroutines)
|
||||||
{
|
{
|
||||||
CharStrings = charStrings ?? throw new ArgumentNullException(nameof(charStrings));
|
CharStrings = charStrings ?? throw new ArgumentNullException(nameof(charStrings));
|
||||||
LocalSubroutines = localSubroutines ?? throw new ArgumentNullException(nameof(localSubroutines));
|
LocalSubroutines = localSubroutines ?? throw new ArgumentNullException(nameof(localSubroutines));
|
||||||
@@ -52,8 +65,6 @@
|
|||||||
{
|
{
|
||||||
glyph = Run(sequence);
|
glyph = Run(sequence);
|
||||||
|
|
||||||
var svg = glyph.ToFullSvg();
|
|
||||||
|
|
||||||
glyphs[name] = glyph;
|
glyphs[name] = glyph;
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
@@ -65,14 +76,13 @@
|
|||||||
return glyph;
|
return glyph;
|
||||||
}
|
}
|
||||||
|
|
||||||
private CharacterPath Run(CommandSequence sequence)
|
private static CharacterPath Run(CommandSequence sequence)
|
||||||
{
|
{
|
||||||
var context = new Type2BuildCharContext(LocalSubroutines, GlobalSubroutines);
|
var context = new Type2BuildCharContext();
|
||||||
|
|
||||||
var hasRunStackClearingCommand = false;
|
var hasRunStackClearingCommand = false;
|
||||||
foreach (var command in sequence.Commands)
|
foreach (var command in sequence.Commands)
|
||||||
{
|
{
|
||||||
context.All.Add(command);
|
|
||||||
command.Match(x => context.Stack.Push(x),
|
command.Match(x => context.Stack.Push(x),
|
||||||
x =>
|
x =>
|
||||||
{
|
{
|
||||||
|
Reference in New Issue
Block a user