vendor: import KillerPDF 1.7.5 source (GPL-3.0) as the base for MMD PDF

This commit is contained in:
2026-08-27 06:58:22 +02:00
commit 532485a830
577 changed files with 149058 additions and 0 deletions
+772
View File
@@ -0,0 +1,772 @@
using System.Diagnostics;
using System.IO;
using System.Runtime.InteropServices;
using System.Text.Json;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Input;
using System.Windows.Interop;
using System.Windows.Media;
using System.Windows.Media.Animation;
using System.Windows.Media.Imaging;
using System.Windows.Shapes;
using Docnet.Core;
using Docnet.Core.Models;
using Microsoft.Win32;
using PdfSharpCore.Drawing;
using PdfSharpCore.Pdf;
using PdfSharpCore.Pdf.IO;
using KillerPDF.Services;
using PdfPigDoc = UglyToad.PdfPig.PdfDocument;
namespace KillerPDF
{
public partial class MainWindow
{
// ============================================================
// Text tool settings bar
// ============================================================
// Underline and/or strikethrough as one decoration collection (either, both, or none).
private static TextDecorationCollection? BuildDecorations(bool underline, bool strike)
{
if (!underline && !strike) return null;
var d = new TextDecorationCollection();
if (underline) foreach (var x in TextDecorations.Underline) d.Add(x);
if (strike) foreach (var x in TextDecorations.Strikethrough) d.Add(x);
return d;
}
// Apply the current typeface + Bold/Italic/Underline/Strikethrough to an in-canvas edit box, so
// editing stays WYSIWYG with the text bar. Bad font names fall back to Segoe UI rather than throwing.
private void StyleEditBox(TextBox tb)
{
try { tb.FontFamily = new FontFamily(_textFontName); } catch { tb.FontFamily = UiKit.UiFont; }
tb.FontWeight = _textBold ? FontWeights.Bold : FontWeights.Normal;
tb.FontStyle = _textItalic ? FontStyles.Italic : FontStyles.Normal;
tb.TextDecorations = BuildDecorations(_textUnderline, _textStrike);
}
private void ApplyTextStyleToActiveBox()
{
if (_activeTextBox is null) return;
_activeTextBox.Foreground = new SolidColorBrush(_textColor);
_activeTextBox.Background = TextEditBackground(); // reflect the chosen fill live
StyleEditBox(_activeTextBox); // typeface + B/I/S live
int pg = _activeTextBox.Tag is int tp ? tp : PageList.SelectedIndex;
double fontCanvas = _textFontSize;
if (_doc is not null && pg >= 0 && _renderDims.TryGetValue(pg, out var rd) && rd.h > 0)
{
double sy = _doc.Pages[pg].Height.Point / rd.h;
if (sy > 0) fontCanvas = _textFontSize / sy;
}
_activeTextBox.FontSize = fontCanvas;
}
// Applies the current text style to whatever is active: the live edit box if one is open,
// otherwise the selected text box (so its color / fill / size can be changed after placing it).
// Opens the full RGB color picker seeded with the current color; applies the result on OK.
private void OpenColorPicker(Color current, Action<Color> apply, Action? refreshBar = null)
{
var dlg = new ColorPickerDialog(this, Color.FromRgb(current.R, current.G, current.B));
// Live-update the annotate bar behind the (modal) dialog whenever the shared palette is edited.
if (refreshBar is not null) dlg.SwatchesChanged += refreshBar;
dlg.ShowDialog();
// dlg.Accepted, NEVER ShowDialog's return: the eyedropper's nested capture modal can
// corrupt the outer dialog frame so ShowDialog returns false after a real OK, and the
// pick was silently dropped - shapes then drew whatever color last got through
// (the "purple/gray rectangles" bug, trace-proven 2026-08-01).
if (dlg.Accepted) apply(dlg.SelectedColor);
}
// Diagonal rainbow fill for the "more colors" swatches that open the picker.
private static LinearGradientBrush RainbowBrush() => new()
{
StartPoint = new Point(0, 0),
EndPoint = new Point(1, 1),
GradientStops =
{
new GradientStop(Colors.Red, 0), new GradientStop(Colors.Yellow, 0.25),
new GradientStop(Colors.Lime, 0.5), new GradientStop(Colors.Cyan, 0.7),
new GradientStop(Colors.Blue, 1)
}
};
private void ApplyTextStyleToSelection()
{
if (_activeTextBox is not null)
{
ApplyTextStyleToActiveBox();
return;
}
// Apply to the primary selection AND every shift-selected text annotation (these can span pages).
var touched = new HashSet<int>();
void Apply(TextAnnotation ta)
{
ta.SetColor(_textColor);
ta.SetFill(_textFillColor);
ta.FontName = _textFontName;
ta.Bold = _textBold;
ta.Italic = _textItalic;
ta.Strike = _textStrike;
ta.Underline = _textUnderline;
double sy = 1.0;
if (_doc is not null && _renderDims.TryGetValue(ta.PageIndex, out var rd) && rd.h > 0)
sy = _doc.Pages[ta.PageIndex].Height.Point / rd.h;
if (sy > 0 && _textFontSize > 0) ta.FontSize = _textFontSize / sy;
touched.Add(ta.PageIndex);
}
if (_selectedAnnotation is TextAnnotation primary) Apply(primary);
foreach (var a in _selectedSet)
if (a is TextAnnotation ta && !ReferenceEquals(ta, _selectedAnnotation)) Apply(ta);
if (touched.Count == 0) return;
MarkDirty();
int primPage = (_selectedAnnotation as TextAnnotation)?.PageIndex ?? -1;
foreach (int p in touched) if (p != primPage) RenderAllAnnotations(p);
if (primPage >= 0)
{
RenderAllAnnotations(primPage); // render primary's page last so its chrome lands on _activeCanvas
if (_selectionBorder is not null) _activeCanvas.Children.Add(_selectionBorder);
foreach (var hd in _resizeHandles) _activeCanvas.Children.Add(hd);
}
}
// Draw-bar counterpart to ApplyTextStyleToSelection: when a highlight / line / ink annotation
// is selected (not just being freshly drawn), push the bar's current color, opacity and width
// onto it and repaint - so editing an existing annotation works the same as setting up a new one.
private void ApplyDrawStyleToSelection()
{
// Apply to the primary selection AND every shift-selected highlight / line / ink annotation.
var touched = new HashSet<int>();
void Apply(PageAnnotation a)
{
if (a is HighlightAnnotation ha)
{
ha.SetColor(ha.Style == HighlightStyle.Fill ? _highlightColor : _lineAnnotColor);
touched.Add(ha.PageIndex);
}
else if (a is InkAnnotation ia)
{
ia.SetColor(_drawColor);
ia.StrokeWidth = _drawWidth;
touched.Add(ia.PageIndex);
}
}
if (_selectedAnnotation is not null) Apply(_selectedAnnotation);
foreach (var a in _selectedSet)
if (!ReferenceEquals(a, _selectedAnnotation)) Apply(a);
if (touched.Count == 0) return;
MarkDirty();
int primPage = _selectedAnnotation?.PageIndex ?? -1;
foreach (int p in touched) if (p != primPage) RenderAllAnnotations(p);
if (primPage >= 0)
{
RenderAllAnnotations(primPage);
ReattachSelectionVisuals();
}
}
private void ShowTextSettings()
{
bool appearing = _annotBarTool != EditTool.Text; // real appear/switch vs same-tool refresh
if (_textSettingsBar is not null)
{
if (appearing) FadeOutAndRemoveBar(_textSettingsBar);
else (PagePreviewPanel.Parent as Grid)?.Children.Remove(_textSettingsBar);
_textSettingsBar = null;
}
// Six self-contained single-row groups. The finished text bar deliberately pairs them
// into two aligned rows: font/style over size, text color over fill color, and text
// opacity over fill opacity. This avoids a lone Fill Opacity group looking like an
// accidental wrap at otherwise comfortable window widths.
StackPanel Group()
{
return new StackPanel
{
Orientation = Orientation.Horizontal,
VerticalAlignment = VerticalAlignment.Center,
// Right margin = the gap to the next group; top/bottom = the gap between wrapped rows.
Margin = new Thickness(0, 3, 16, 3)
};
}
TextBlock DimLabel(string text, int top, bool rightAlign = false)
{
var t = new TextBlock
{
Text = text,
FontFamily = UiKit.UiFont,
FontSize = 11,
VerticalAlignment = VerticalAlignment.Center,
HorizontalAlignment = rightAlign ? HorizontalAlignment.Right : HorizontalAlignment.Left,
Margin = new Thickness(0, top, 6, 0)
};
t.SetResourceReference(TextBlock.ForegroundProperty, "MutedTextBrush");
return t;
}
Border ColorSwatch(Color c, bool isActive, MouseButtonEventHandler onClick)
{
var sw = new Border
{
Width = 18,
Height = 18,
Background = new SolidColorBrush(c),
BorderThickness = new Thickness(isActive ? 2 : 1),
CornerRadius = new CornerRadius(3),
Margin = new Thickness(1),
Cursor = Cursors.Hand
};
if (isActive) sw.SetResourceReference(Border.BorderBrushProperty, "PrimaryBrush");
else sw.BorderBrush = _swatchDimBorder;
sw.MouseLeftButtonDown += onClick;
return sw;
}
// Opens the full RGB picker. When the current color isn't one of the presets it shows that
// color with an accent ring (and a small rainbow corner), so the bar reflects a custom pick.
Grid MoreColorsSwatch(Color current, bool customActive, MouseButtonEventHandler onClick)
{
var grid = new Grid { Width = 18, Height = 18, Margin = new Thickness(1), Cursor = Cursors.Hand, ToolTip = Loc("Str_Bar_MoreColors") };
var bg = new Border
{
CornerRadius = new CornerRadius(3),
BorderThickness = new Thickness(customActive ? 2 : 1),
Background = customActive
? (Brush)new SolidColorBrush(Color.FromRgb(current.R, current.G, current.B))
: RainbowBrush()
};
if (customActive) bg.SetResourceReference(Border.BorderBrushProperty, "PrimaryBrush"); else bg.BorderBrush = _swatchDimBorder;
grid.Children.Add(bg);
if (customActive)
grid.Children.Add(new System.Windows.Shapes.Polygon
{
Points = [new Point(18, 7), new Point(18, 18), new Point(7, 18)],
Fill = RainbowBrush(),
IsHitTestVisible = false
});
grid.MouseLeftButtonDown += onClick;
return grid;
}
// Drag grip, centered vertically against whatever height the wrapped rows produce.
var textGrip = MakeBarGrip(4);
textGrip.VerticalAlignment = VerticalAlignment.Center;
// (grip is added to the WrapPanel host directly, below.)
var swatchRow1 = new StackPanel { Orientation = Orientation.Horizontal };
foreach (var color in SwatchColors)
{
var c = color;
bool isActive = c.R == _textColor.R && c.G == _textColor.G && c.B == _textColor.B;
swatchRow1.Children.Add(ColorSwatch(c, isActive, (_, _) =>
{
_textColor = Color.FromArgb(_textOpacity, c.R, c.G, c.B);
ApplyTextStyleToSelection();
ShowTextSettings();
}));
}
bool textCustom = !SwatchColors.Any(sc => sc.R == _textColor.R && sc.G == _textColor.G && sc.B == _textColor.B);
swatchRow1.Children.Add(MoreColorsSwatch(_textColor, textCustom, (_, _) => OpenColorPicker(_textColor, c =>
{
_textColor = Color.FromArgb(_textOpacity, c.R, c.G, c.B);
ApplyTextStyleToSelection();
ShowTextSettings();
}, () => ShowTextSettings())));
var grpColor = Group();
grpColor.Children.Add(DimLabel(Loc("Str_Bar_Color"), 0));
grpColor.Children.Add(swatchRow1);
var swatchRow2 = new StackPanel { Orientation = Orientation.Horizontal };
bool noneActive = _textFillColor.A == 0;
var noneGrid = new Grid { Width = 18, Height = 18, Margin = new Thickness(1), Cursor = Cursors.Hand };
var noneBg = new Border { CornerRadius = new CornerRadius(3), Background = Brushes.White, BorderThickness = new Thickness(noneActive ? 2 : 1) };
if (noneActive) noneBg.SetResourceReference(Border.BorderBrushProperty, "PrimaryBrush"); else noneBg.BorderBrush = _swatchDimBorder;
noneGrid.Children.Add(noneBg);
noneGrid.Children.Add(new System.Windows.Shapes.Line { X1 = 3, Y1 = 15, X2 = 15, Y2 = 3, Stroke = Brushes.Red, StrokeThickness = 1.5 });
noneGrid.MouseLeftButtonDown += (_, _) =>
{
_textFillColor = Color.FromArgb(0, _textFillColor.R, _textFillColor.G, _textFillColor.B);
ApplyTextStyleToSelection();
ShowTextSettings();
};
swatchRow2.Children.Add(noneGrid);
foreach (var color in SwatchColors)
{
var c = color;
bool isActive = _textFillColor.A > 0 && c.R == _textFillColor.R && c.G == _textFillColor.G && c.B == _textFillColor.B;
swatchRow2.Children.Add(ColorSwatch(c, isActive, (_, _) =>
{
byte a = _textFillColor.A == 0 ? (byte)255 : _textFillColor.A; // enable at full/current opacity
_textFillColor = Color.FromArgb(a, c.R, c.G, c.B);
ApplyTextStyleToSelection();
ShowTextSettings();
}));
}
bool fillCustom = _textFillColor.A > 0 && !SwatchColors.Any(sc => sc.R == _textFillColor.R && sc.G == _textFillColor.G && sc.B == _textFillColor.B);
swatchRow2.Children.Add(MoreColorsSwatch(_textFillColor.A == 0 ? Colors.White : _textFillColor, fillCustom, (_, _) => OpenColorPicker(_textFillColor.A == 0 ? Colors.White : _textFillColor, c =>
{
byte a = _textFillColor.A == 0 ? (byte)255 : _textFillColor.A;
_textFillColor = Color.FromArgb(a, c.R, c.G, c.B);
ApplyTextStyleToSelection();
ShowTextSettings();
}, () => ShowTextSettings())));
var grpFill = Group();
grpFill.Children.Add(DimLabel(Loc("Str_Bar_Fill"), 0));
grpFill.Children.Add(swatchRow2);
// Size group: its own single-row group, packed beside Font whenever the width allows.
var sizeStack = Group();
sizeStack.Children.Add(DimLabel(Loc("Str_Bar_Size"), 0));
var sizeSlider = new Slider
{
Minimum = 8,
Maximum = 72,
Value = Math.Max(8, Math.Min(72, _textFontSize)),
Width = 90,
VerticalAlignment = VerticalAlignment.Center,
TickFrequency = 1,
IsSnapToTickEnabled = true,
Style = (Style)FindResource("DarkSlider")
};
// Editable size box (type an exact value; the slider stays for quick coarse adjustment).
var sizeBox = new TextBox
{
Text = $"{_textFontSize:F0}",
FontFamily = UiKit.UiFont,
FontSize = 11,
Width = 32,
MaxLength = 4,
VerticalAlignment = VerticalAlignment.Center,
VerticalContentAlignment = VerticalAlignment.Center,
TextAlignment = TextAlignment.Center,
Margin = new Thickness(4, 0, 0, 0),
BorderThickness = new Thickness(1),
Template = FlatTextBoxTemplate()
};
sizeBox.SetResourceReference(TextBox.BackgroundProperty, "PaneBrush");
sizeBox.SetResourceReference(TextBox.ForegroundProperty, "TextBrush");
sizeBox.SetResourceReference(TextBox.BorderBrushProperty, "CardBorderBrush");
sizeBox.SetResourceReference(TextBox.CaretBrushProperty, "PrimaryBrush");
sizeBox.SetResourceReference(TextBox.SelectionBrushProperty, "RowSelectedBrush"); // no WPF-default blue
var ptLabel = new TextBlock
{
Text = "pt",
FontFamily = UiKit.UiFont,
FontSize = 11,
VerticalAlignment = VerticalAlignment.Center,
Margin = new Thickness(2, 0, 0, 0)
};
ptLabel.SetResourceReference(TextBlock.ForegroundProperty, "MutedTextBrush");
// Slider drives the box; the box drives the slider. _suppressSizeSync breaks the feedback loop.
sizeSlider.ValueChanged += (s, e) =>
{
if (_suppressSizeSync) return;
_textFontSize = e.NewValue;
sizeBox.Text = $"{e.NewValue:F0}";
ApplyTextStyleToSelection();
};
void CommitSizeBox()
{
if (double.TryParse(sizeBox.Text, out double v))
{
_textFontSize = Math.Max(1, Math.Min(400, Math.Round(v)));
_suppressSizeSync = true;
sizeSlider.Value = Math.Max(8, Math.Min(72, _textFontSize)); // thumb clamps; box keeps exact
_suppressSizeSync = false;
ApplyTextStyleToSelection();
}
sizeBox.Text = $"{_textFontSize:F0}"; // normalize / revert invalid input
}
// Set an exact size and keep the slider + box in step (slider thumb clamps to 8-72).
void SetSize(double v)
{
_textFontSize = Math.Max(1, Math.Min(400, Math.Round(v)));
_suppressSizeSync = true;
sizeSlider.Value = Math.Max(8, Math.Min(72, _textFontSize));
_suppressSizeSync = false;
sizeBox.Text = $"{_textFontSize:F0}";
ApplyTextStyleToSelection();
}
// Tiny stepper button ( / +) for one-point nudges next to the slider.
Border StepButton(string glyph, Action onClick)
{
var st = new TextBlock
{
Text = glyph,
FontFamily = UiKit.UiFont,
FontSize = 13,
VerticalAlignment = VerticalAlignment.Center,
HorizontalAlignment = HorizontalAlignment.Center
};
st.SetResourceReference(TextBlock.ForegroundProperty, "TextBrush");
var sb = new Border
{
Width = 18,
Height = 20,
CornerRadius = new CornerRadius(3),
Margin = new Thickness(3, 0, 0, 0),
Cursor = Cursors.Hand,
BorderThickness = new Thickness(1),
Background = Brushes.Transparent,
Child = st,
BorderBrush = _swatchDimBorder
};
sb.MouseLeftButtonDown += (_, _) => onClick();
return sb;
}
sizeBox.PreviewKeyDown += (s, e) =>
{
if (e.Key == Key.Enter) { CommitSizeBox(); e.Handled = true; }
else if (e.Key == Key.Escape) { sizeBox.Text = $"{_textFontSize:F0}"; e.Handled = true; }
};
sizeBox.LostFocus += (s, e) => CommitSizeBox();
sizeBox.GotFocus += (s, e) => sizeBox.SelectAll();
sizeStack.Children.Add(sizeSlider);
sizeStack.Children.Add(StepButton("", () => SetSize(_textFontSize - 1))); // minus
sizeStack.Children.Add(StepButton("+", () => SetSize(_textFontSize + 1)));
sizeStack.Children.Add(sizeBox);
sizeStack.Children.Add(ptLabel);
// Font group: typeface selector + Bold / Italic / Strikethrough / Underline.
// A small square toggle whose glyph previews its own effect (bold B, italic I, struck-through S).
Border StyleToggle(string glyph, string tip, bool active, FontWeight fw, FontStyle fs, TextDecorationCollection? deco, Action onClick)
{
var gt = new TextBlock
{
Text = glyph,
FontFamily = UiKit.UiFont,
FontSize = 12,
FontWeight = fw,
FontStyle = fs,
TextDecorations = deco,
VerticalAlignment = VerticalAlignment.Center,
HorizontalAlignment = HorizontalAlignment.Center
};
gt.SetResourceReference(TextBlock.ForegroundProperty, "TextBrush");
var b = new Border
{
Width = 22,
Height = 20,
CornerRadius = new CornerRadius(3),
Margin = new Thickness(2, 0, 0, 0),
Cursor = Cursors.Hand,
ToolTip = tip,
BorderThickness = new Thickness(active ? 2 : 1),
Background = active ? AccentBrush(40) : Brushes.Transparent,
Child = gt
};
if (active) b.SetResourceReference(Border.BorderBrushProperty, "PrimaryBrush"); else b.BorderBrush = _swatchDimBorder;
b.MouseLeftButtonDown += (_, _) => onClick();
return b;
}
var fontStack = Group();
fontStack.Children.Add(DimLabel(Loc("Str_Bar_Font"), 0));
var fontBox = new ComboBox
{
Width = 132,
Height = 22,
VerticalAlignment = VerticalAlignment.Center,
FontFamily = UiKit.UiFont,
FontSize = 11,
MaxDropDownHeight = 320,
Margin = new Thickness(0, 0, 4, 0)
};
if (FindResource("DarkComboBox") is Style cbStyle) fontBox.Style = cbStyle;
if (FindResource("DarkComboItem") is Style ciStyle) fontBox.ItemContainerStyle = ciStyle;
foreach (var fn in SystemFontNames) fontBox.Items.Add(fn);
fontBox.SelectedItem = _textFontName;
fontBox.SelectionChanged += (s, e) =>
{
if (fontBox.SelectedItem is string fn) { _textFontName = fn; ApplyTextStyleToSelection(); }
};
fontStack.Children.Add(fontBox);
fontStack.Children.Add(StyleToggle("B", Loc("Str_Lbl_Bold"), _textBold, FontWeights.Bold, FontStyles.Normal, null,
() => { _textBold = !_textBold; ApplyTextStyleToSelection(); ShowTextSettings(); }));
fontStack.Children.Add(StyleToggle("I", Loc("Str_Lbl_Italic"), _textItalic, FontWeights.Normal, FontStyles.Italic, null,
() => { _textItalic = !_textItalic; ApplyTextStyleToSelection(); ShowTextSettings(); }));
fontStack.Children.Add(StyleToggle("S", Loc("Str_Lbl_Strike"), _textStrike, FontWeights.Normal, FontStyles.Normal, TextDecorations.Strikethrough,
() => { _textStrike = !_textStrike; ApplyTextStyleToSelection(); ShowTextSettings(); }));
fontStack.Children.Add(StyleToggle("U", Loc("Str_Lbl_Underline"), _textUnderline, FontWeights.Normal, FontStyles.Normal, TextDecorations.Underline,
() => { _textUnderline = !_textUnderline; ApplyTextStyleToSelection(); ShowTextSettings(); }));
// Opacity and Fill Opacity: two independent single-row groups.
var grpOpacity = Group();
grpOpacity.Children.Add(DimLabel(Loc("Str_Bar_Opacity"), 0));
var opacitySlider = new Slider
{
Minimum = 10,
Maximum = 255,
Value = _textOpacity,
Width = 90,
VerticalAlignment = VerticalAlignment.Center,
Style = (Style)FindResource("DarkSlider")
};
grpOpacity.Children.Add(opacitySlider);
var opacityLabel = new TextBlock
{
Text = $"{(int)(_textOpacity / 255.0 * 100)}%",
FontFamily = UiKit.UiFont,
FontSize = 11,
VerticalAlignment = VerticalAlignment.Center,
Margin = new Thickness(4, 0, 0, 0),
Width = 40,
TextAlignment = TextAlignment.Right
};
opacityLabel.SetResourceReference(TextBlock.ForegroundProperty, "MutedTextBrush");
grpOpacity.Children.Add(opacityLabel);
opacitySlider.ValueChanged += (s, e) =>
{
byte a = (byte)e.NewValue;
opacityLabel.Text = $"{(int)(a / 255.0 * 100)}%";
_textOpacity = a;
_textColor = Color.FromArgb(a, _textColor.R, _textColor.G, _textColor.B);
ApplyTextStyleToSelection();
};
var grpFillOp = Group();
grpFillOp.Children.Add(DimLabel(Loc("Str_Bar_FillOpacity"), 0));
byte curFillA = _textFillColor.A == 0 ? (byte)255 : _textFillColor.A;
var fillOpSlider = new Slider
{
Minimum = 10,
Maximum = 255,
Value = curFillA,
Width = 90,
VerticalAlignment = VerticalAlignment.Center,
Style = (Style)FindResource("DarkSlider")
};
grpFillOp.Children.Add(fillOpSlider);
var fillOpLabel = new TextBlock
{
Text = $"{(int)(curFillA / 255.0 * 100)}%",
FontFamily = UiKit.UiFont,
FontSize = 11,
VerticalAlignment = VerticalAlignment.Center,
Margin = new Thickness(4, 0, 0, 0),
Width = 40,
TextAlignment = TextAlignment.Right
};
fillOpLabel.SetResourceReference(TextBlock.ForegroundProperty, "MutedTextBrush");
grpFillOp.Children.Add(fillOpLabel);
fillOpSlider.ValueChanged += (s, e) =>
{
byte a = (byte)e.NewValue;
fillOpLabel.Text = $"{(int)(a / 255.0 * 100)}%";
// Dragging opacity turns the fill on (defaults to the current color, white for whiteout).
_textFillColor = Color.FromArgb(a, _textFillColor.R, _textFillColor.G, _textFillColor.B);
ApplyTextStyleToSelection();
};
// Intentional two-row interim layout for 1.7.5, assembled as three vertical pairs so
// related controls share an exact left edge: Font over Size, Text Color over Fill, and
// Text Opacity over Fill Opacity. The outer WrapPanel moves a whole pair at narrow split-
// pane widths instead of separating a label from the control directly beneath it.
var fontPair = new StackPanel();
fontPair.Children.Add(fontStack);
fontPair.Children.Add(sizeStack);
var colorPair = new StackPanel();
colorPair.Children.Add(grpColor);
colorPair.Children.Add(grpFill);
var opacityPair = new StackPanel();
opacityPair.Children.Add(grpOpacity);
opacityPair.Children.Add(grpFillOp);
var pairHost = new WrapPanel
{
Orientation = Orientation.Horizontal,
Margin = new Thickness(8, 2, 8, 2),
Background = Brushes.Transparent
};
pairHost.Children.Add(textGrip);
pairHost.Children.Add(fontPair);
pairHost.Children.Add(colorPair);
pairHost.Children.Add(opacityPair);
_annotBarDragInners.Clear();
_textSettingsBar = new Border
{
BorderThickness = new Thickness(1, 0, 1, 1), // no top border - the toolbar above already separates
HorizontalAlignment = HorizontalAlignment.Right, // right-anchored; slid via the grip
VerticalAlignment = VerticalAlignment.Top,
CornerRadius = new CornerRadius(0),
Padding = new Thickness(4),
Effect = AnnotBarShadow(),
Child = BuildBarHost(pairHost),
Margin = new Thickness(0, 0, 0, 0)
};
_textSettingsBar.SetResourceReference(Border.BackgroundProperty, "BgFlyout");
_textSettingsBar.SetResourceReference(Border.BorderBrushProperty, "PaneBorderBrush");
_textSettingsBar.SetResourceReference(Border.CornerRadiusProperty, "AnnotationBarCornerRadius");
var previewArea = PagePreviewPanel.Parent as Grid;
if (previewArea is not null)
{
Panel.SetZIndex(_textSettingsBar, 100);
previewArea.Children.Add(_textSettingsBar);
// Cap the paired layout to the document area. At split-pane widths whole columns wrap
// together; at normal widths the two rows retain their deliberate vertical alignment.
pairHost.SetBinding(FrameworkElement.MaxWidthProperty, new System.Windows.Data.Binding("ActualWidth")
{ Source = previewArea, Converter = _barWidthInset });
WireBarWrapAdaptation(pairHost, textGrip, fontPair, previewArea);
PlaceAnnotationBar(_textSettingsBar, textGrip, fadeIn: appearing);
}
_annotBarTool = EditTool.Text;
_annotBarMinimized = false; // a freshly built bar is full-size
}
// (The shared overflow machinery - MakeBarOverflow and WireBarOverflow - lives in
// AnnotationBars.cs, used by this bar and the draw/highlight/line/shape bar alike.)
private void HideTextSettings()
{
FadeOutAndRemoveBar(_textSettingsBar);
_textSettingsBar = null;
if (_annotBarTool == EditTool.Text) _annotBarTool = null;
}
private void PlaceImageFromDialog(Point pos, int pageIdx)
{
var dlg = new Controls.FileDialog(Controls.FileDialogMode.Open)
{
Title = Loc("Str_Dlg_InsertImage"),
Filter = Loc("Str_Filter_Images") + "|*.png;*.jpg;*.jpeg;*.bmp;*.gif;*.tiff;*.tif|" + Loc("Str_Filter_AllFiles") + "|*.*",
ShowImagePreview = true
};
if (dlg.ShowDialog(this) != true) return;
try
{
var imgBytes = File.ReadAllBytes(dlg.FileName);
var bmp = new BitmapImage();
bmp.BeginInit();
bmp.StreamSource = new MemoryStream(imgBytes);
bmp.CacheOption = BitmapCacheOption.OnLoad;
bmp.EndInit();
double srcW = bmp.PixelWidth > 0 ? bmp.PixelWidth : 400;
double srcH = bmp.PixelHeight > 0 ? bmp.PixelHeight : 300;
// Default the placed image to ~50% of the page's longest side (in render-dim
// units) so it is a usable size regardless of page dimensions, never upscaling
// beyond the source's native resolution.
double pageMax = _renderDims.TryGetValue(pageIdx, out var rdImg)
? Math.Max(rdImg.w, rdImg.h) : 2048.0;
double MaxCanvasDim = pageMax * 0.5;
double scale = Math.Min(1.0, Math.Min(MaxCanvasDim / srcW, MaxCanvasDim / srcH));
var imgAnnot = new ImageAnnotation
{
PageIndex = pageIdx,
Position = pos,
Scale = scale,
SourceWidth = srcW,
SourceHeight = srcH,
ImageData = Convert.ToBase64String(imgBytes)
};
// Switch to Select FIRST so placement renders last and nothing wipes the image
// (calling SetTool between render and select was what made the image vanish).
SetTool(EditTool.Select);
AddAnnotation(imgAnnot);
RenderAllAnnotations(pageIdx);
double w = srcW * scale;
double h = srcH * scale;
SelectAnnotation(imgAnnot, new Rect(pos.X, pos.Y, w, h));
SetStatus(Loc("Str_St_ImagePlaced"));
}
catch (Exception ex)
{
KillerDialog.Show(this, Loc("Str_Err_LoadImageFailed") + "\n" + ex.Message, "KillerPDF", MessageBoxButton.OK, MessageBoxImage.Error);
}
}
// Ctrl+V: drop a clipboard image (as an image annotation) or clipboard text (as a text
// annotation) onto the current page, centered, then select it. Coordinates are in the page's
// render-dim space (== _renderDims[page]), matching how clicks place annotations.
private void PasteFromClipboard()
{
if (_doc is null) return;
int pageIdx = PageList.SelectedIndex;
if (pageIdx < 0) pageIdx = 0;
if (pageIdx >= _doc.PageCount) return;
double pw = _renderDims.TryGetValue(pageIdx, out var rd) ? rd.w : 2048.0;
double ph = _renderDims.TryGetValue(pageIdx, out var rd2) ? rd2.h : 2048.0;
try
{
if (Clipboard.ContainsImage())
{
var src = Clipboard.GetImage();
if (src is null) { SetStatus(Loc("Str_St_ClipImageUnreadable")); return; }
// Encode to PNG so ImageAnnotation stores standard bytes (same as file import).
byte[] imgBytes;
var encoder = new System.Windows.Media.Imaging.PngBitmapEncoder();
encoder.Frames.Add(System.Windows.Media.Imaging.BitmapFrame.Create(src));
using (var ms = new MemoryStream()) { encoder.Save(ms); imgBytes = ms.ToArray(); }
double srcW = src.PixelWidth > 0 ? src.PixelWidth : 400;
double srcH = src.PixelHeight > 0 ? src.PixelHeight : 300;
double pageMax = Math.Max(pw, ph);
double maxCanvasDim = pageMax * 0.5;
double scale = Math.Min(1.0, Math.Min(maxCanvasDim / srcW, maxCanvasDim / srcH));
double w = srcW * scale, h = srcH * scale;
var pos = new Point((pw - w) / 2, (ph - h) / 2);
var imgAnnot = new ImageAnnotation
{
PageIndex = pageIdx,
Position = pos,
Scale = scale,
SourceWidth = srcW,
SourceHeight = srcH,
ImageData = Convert.ToBase64String(imgBytes)
};
SetTool(EditTool.Select);
AddAnnotation(imgAnnot);
RenderAllAnnotations(pageIdx);
SelectAnnotation(imgAnnot, new Rect(pos.X, pos.Y, w, h));
SetStatus(Loc("Str_St_PastedImage"));
}
else if (Clipboard.ContainsText())
{
string content = Clipboard.GetText().Trim();
if (string.IsNullOrEmpty(content)) { SetStatus(Loc("Str_St_ClipNoText")); return; }
// Convert the point size to the page's canvas units (see PlaceTextBox).
double fontCanvas = _textFontSize;
double sy = _doc.Pages[pageIdx].Height.Point / Math.Max(1.0, ph);
if (sy > 0) fontCanvas = _textFontSize / sy;
var ta = new TextAnnotation
{
PageIndex = pageIdx,
Position = new Point(pw * 0.25, ph * 0.45),
Content = content,
FontSize = fontCanvas
};
ta.SetColor(_textColor);
SetTool(EditTool.Select);
AddAnnotation(ta);
RenderAllAnnotations(pageIdx);
SelectAnnotation(ta, AnnotBounds(ta));
SetStatus(Loc("Str_St_PastedText"));
}
else
{
SetStatus(Loc("Str_St_ClipEmpty"));
}
}
catch (Exception ex)
{
KillerDialog.Show(this, Loc("Str_Err_PasteFailed") + "\n" + ex.Message, "KillerPDF", MessageBoxButton.OK, MessageBoxImage.Error);
}
}
}
}