mirror of
https://github.com/Frooodle/Stirling-PDF.git
synced 2025-09-08 17:51:20 +02:00
feat(misc): Add font color option for page numbers; improve alignment & robustness (#4334)
# Description of Changes **What was changed** - **API & backend** - Added optional `fontColor` (hex, e.g. `#FF0000`) to `AddPageNumbersRequest` with OpenAPI docs, default `#000000`. - Decode hex color with safe fallback to black; apply via `setNonStrokingColor`. - Switched multiple `switch` statements to concise switch expressions and used `Locale.ROOT` for case operations. - Clamped `position` to `1..9` and reworked alignment using proper font metrics (`ascent`/`descent`) for top/middle/bottom positioning. - Centralized filename base extraction; reduced repeated calls. - Used try-with-resources for `PDPageContentStream`. - **UI & i18n** - Added `addPageNumbers.fontColor` label (en_GB). - Introduced `<input type="color" id="fontColor" ...>` with live background preview in the Add Page Numbers form. **Why the change was made** - Enable users to render page numbers in a chosen color (not just black). - Produce visually correct placement by accounting for font metrics (baseline vs. optical middle). - Improve resilience (locale-safe parsing, bounds checking) and code clarity. Closes #3839 [after.pdf](https://github.com/user-attachments/files/22064425/after.pdf) [before.pdf](https://github.com/user-attachments/files/22064426/before.pdf) --- ## Checklist ### General - [x] I have read the [Contribution Guidelines](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/CONTRIBUTING.md) - [x] I have read the [Stirling-PDF Developer Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/DeveloperGuide.md) (if applicable) - [ ] I have read the [How to add new languages to Stirling-PDF](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md) (if applicable) - [x] I have performed a self-review of my own code - [x] My changes generate no new warnings ### Documentation - [ ] I have updated relevant docs on [Stirling-PDF's doc repo](https://github.com/Stirling-Tools/Stirling-Tools.github.io/blob/main/docs/) (if functionality has heavily changed) - [ ] I have read the section [Add New Translation Tags](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md#add-new-translation-tags) (for new translation tags only) ### UI Changes (if applicable) - [ ] Screenshots or videos demonstrating the UI changes are attached (e.g., as comments or direct attachments in the PR) ### Testing (if applicable) - [ ] I have tested my changes locally. Refer to the [Testing Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/DeveloperGuide.md#6-testing) for more details.
This commit is contained in:
parent
a4a57cef92
commit
9a213c4bf6
@ -1,8 +1,10 @@
|
||||
package stirling.software.SPDF.controller.api.misc;
|
||||
|
||||
import java.awt.Color;
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
|
||||
import org.apache.pdfbox.pdmodel.PDDocument;
|
||||
import org.apache.pdfbox.pdmodel.PDPage;
|
||||
@ -54,24 +56,27 @@ public class PageNumbersController {
|
||||
String customText = request.getCustomText();
|
||||
float fontSize = request.getFontSize();
|
||||
String fontType = request.getFontType();
|
||||
String fontColor = request.getFontColor();
|
||||
|
||||
Color color = Color.BLACK;
|
||||
if (fontColor != null && !fontColor.trim().isEmpty()) {
|
||||
try {
|
||||
color = Color.decode(fontColor);
|
||||
} catch (NumberFormatException e) {
|
||||
color = Color.BLACK;
|
||||
}
|
||||
}
|
||||
|
||||
PDDocument document = pdfDocumentFactory.load(file);
|
||||
float marginFactor;
|
||||
switch (customMargin.toLowerCase()) {
|
||||
case "small":
|
||||
marginFactor = 0.02f;
|
||||
break;
|
||||
case "large":
|
||||
marginFactor = 0.05f;
|
||||
break;
|
||||
case "x-large":
|
||||
marginFactor = 0.075f;
|
||||
break;
|
||||
case "medium":
|
||||
default:
|
||||
marginFactor = 0.035f;
|
||||
break;
|
||||
}
|
||||
|
||||
float marginFactor =
|
||||
switch (customMargin == null ? "" : customMargin.toLowerCase(Locale.ROOT)) {
|
||||
case "small" -> 0.02f;
|
||||
case "large" -> 0.05f;
|
||||
case "x-large" -> 0.075f;
|
||||
case "medium" -> 0.035f;
|
||||
default -> 0.035f;
|
||||
};
|
||||
|
||||
if (pagesToNumber == null || pagesToNumber.isEmpty()) {
|
||||
pagesToNumber = "all";
|
||||
@ -79,9 +84,17 @@ public class PageNumbersController {
|
||||
if (customText == null || customText.isEmpty()) {
|
||||
customText = "{n}";
|
||||
}
|
||||
|
||||
final String baseFilename =
|
||||
Filenames.toSimpleFileName(file.getOriginalFilename())
|
||||
.replaceFirst("[.][^.]+$", "");
|
||||
|
||||
List<Integer> pagesToNumberList =
|
||||
GeneralUtils.parsePageList(pagesToNumber.split(","), document.getNumberOfPages());
|
||||
|
||||
// Clamp position to 1..9 (1 = top-left, 9 = bottom-right)
|
||||
int pos = Math.max(1, Math.min(9, position));
|
||||
|
||||
for (int i : pagesToNumberList) {
|
||||
PDPage page = document.getPage(i);
|
||||
PDRectangle pageSize = page.getMediaBox();
|
||||
@ -90,70 +103,62 @@ public class PageNumbersController {
|
||||
customText
|
||||
.replace("{n}", String.valueOf(pageNumber))
|
||||
.replace("{total}", String.valueOf(document.getNumberOfPages()))
|
||||
.replace(
|
||||
"{filename}",
|
||||
Filenames.toSimpleFileName(file.getOriginalFilename())
|
||||
.replaceFirst("[.][^.]+$", ""));
|
||||
.replace("{filename}", baseFilename);
|
||||
|
||||
PDType1Font currentFont =
|
||||
switch (fontType.toLowerCase()) {
|
||||
switch (fontType == null ? "" : fontType.toLowerCase(Locale.ROOT)) {
|
||||
case "courier" -> new PDType1Font(Standard14Fonts.FontName.COURIER);
|
||||
case "times" -> new PDType1Font(Standard14Fonts.FontName.TIMES_ROMAN);
|
||||
default -> new PDType1Font(Standard14Fonts.FontName.HELVETICA);
|
||||
};
|
||||
|
||||
float x, y;
|
||||
// Text dimensions and font metrics
|
||||
float textWidth = currentFont.getStringWidth(text) / 1000f * fontSize;
|
||||
float ascent = currentFont.getFontDescriptor().getAscent() / 1000f * fontSize;
|
||||
float descent = currentFont.getFontDescriptor().getDescent() / 1000f * fontSize;
|
||||
|
||||
if (position == 5) {
|
||||
// Calculate text width and font metrics
|
||||
float textWidth = currentFont.getStringWidth(text) / 1000 * fontSize;
|
||||
// Derive column/row in range 1..3 (1 = left/top, 2 = center/middle, 3 = right/bottom)
|
||||
int col = ((pos - 1) % 3) + 1; // 1 = left, 2 = center, 3 = right
|
||||
int row = ((pos - 1) / 3) + 1; // 1 = top, 2 = middle, 3 = bottom
|
||||
|
||||
float ascent = currentFont.getFontDescriptor().getAscent() / 1000 * fontSize;
|
||||
float descent = currentFont.getFontDescriptor().getDescent() / 1000 * fontSize;
|
||||
// Anchor coordinates with margin
|
||||
float leftX = pageSize.getLowerLeftX() + marginFactor * pageSize.getWidth();
|
||||
float midX = pageSize.getLowerLeftX() + pageSize.getWidth() / 2f;
|
||||
float rightX = pageSize.getUpperRightX() - marginFactor * pageSize.getWidth();
|
||||
|
||||
float centerX = pageSize.getLowerLeftX() + (pageSize.getWidth() / 2);
|
||||
float centerY = pageSize.getLowerLeftY() + (pageSize.getHeight() / 2);
|
||||
float botY = pageSize.getLowerLeftY() + marginFactor * pageSize.getHeight();
|
||||
float midY = pageSize.getLowerLeftY() + pageSize.getHeight() / 2f;
|
||||
float topY = pageSize.getUpperRightY() - marginFactor * pageSize.getHeight();
|
||||
|
||||
x = centerX - (textWidth / 2);
|
||||
y = centerY - (ascent + descent) / 2;
|
||||
} else {
|
||||
int xGroup = (position - 1) % 3;
|
||||
int yGroup = 2 - (position - 1) / 3;
|
||||
|
||||
x =
|
||||
switch (xGroup) {
|
||||
case 0 ->
|
||||
pageSize.getLowerLeftX()
|
||||
+ marginFactor * pageSize.getWidth(); // left
|
||||
case 1 ->
|
||||
pageSize.getLowerLeftX() + (pageSize.getWidth() / 2); // center
|
||||
default ->
|
||||
pageSize.getUpperRightX()
|
||||
- marginFactor * pageSize.getWidth(); // right
|
||||
// Horizontal alignment: left = anchor, center = centered, right = right-aligned
|
||||
float x =
|
||||
switch (col) {
|
||||
case 1 -> leftX;
|
||||
case 2 -> midX - textWidth / 2f;
|
||||
default -> rightX - textWidth;
|
||||
};
|
||||
|
||||
y =
|
||||
switch (yGroup) {
|
||||
case 0 ->
|
||||
pageSize.getLowerLeftY()
|
||||
+ marginFactor * pageSize.getHeight(); // bottom
|
||||
case 1 ->
|
||||
pageSize.getLowerLeftY() + (pageSize.getHeight() / 2); // middle
|
||||
default ->
|
||||
pageSize.getUpperRightY()
|
||||
- marginFactor * pageSize.getHeight(); // top
|
||||
// Vertical alignment (baseline!):
|
||||
// top = align text top at topY,
|
||||
// middle = optical middle using ascent/descent,
|
||||
// bottom = baseline at botY
|
||||
float y =
|
||||
switch (row) {
|
||||
case 1 -> topY - ascent;
|
||||
case 2 -> midY - (ascent + descent) / 2f;
|
||||
default -> botY;
|
||||
};
|
||||
}
|
||||
|
||||
PDPageContentStream contentStream =
|
||||
try (PDPageContentStream contentStream =
|
||||
new PDPageContentStream(
|
||||
document, page, PDPageContentStream.AppendMode.APPEND, true, true);
|
||||
document, page, PDPageContentStream.AppendMode.APPEND, true, true)) {
|
||||
contentStream.beginText();
|
||||
contentStream.setFont(currentFont, fontSize);
|
||||
contentStream.setNonStrokingColor(color);
|
||||
contentStream.newLineAtOffset(x, y);
|
||||
contentStream.showText(text);
|
||||
contentStream.endText();
|
||||
contentStream.close();
|
||||
}
|
||||
|
||||
pageNumber++;
|
||||
}
|
||||
|
@ -32,6 +32,13 @@ public class AddPageNumbersRequest extends PDFWithPageNums {
|
||||
requiredMode = RequiredMode.REQUIRED)
|
||||
private String fontType;
|
||||
|
||||
@Schema(
|
||||
description = "Hex colour for page numbers (e.g. #FF0000)",
|
||||
example = "#000000",
|
||||
defaultValue = "#000000",
|
||||
requiredMode = RequiredMode.NOT_REQUIRED)
|
||||
private String fontColor;
|
||||
|
||||
@Schema(
|
||||
description =
|
||||
"Position: 1-9 representing positions on the page (1=top-left, 2=top-center,"
|
||||
|
@ -137,6 +137,7 @@ lang.yor=Yoruba
|
||||
|
||||
addPageNumbers.fontSize=Font Size
|
||||
addPageNumbers.fontName=Font Name
|
||||
addPageNumbers.fontColor=Font Colour
|
||||
pdfPrompt=Select PDF(s)
|
||||
multiPdfPrompt=Select PDFs (2+)
|
||||
multiPdfDropPrompt=Select (or drag & drop) all PDFs you require
|
||||
|
@ -99,6 +99,24 @@
|
||||
<option value="Courier">Courier New</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="fontColor" th:text="#{addPageNumbers.fontColor}"></label>
|
||||
<div class="form-control form-control-color" style="background-color: #000000;">
|
||||
<input type="color" id="fontColor" name="fontColor" value="#000000">
|
||||
</div>
|
||||
<script>
|
||||
let colorInput = document.getElementById("fontColor");
|
||||
if (colorInput) {
|
||||
let colorInputContainer = colorInput.parentElement;
|
||||
if (colorInputContainer) {
|
||||
colorInput.onchange = function() {
|
||||
colorInputContainer.style.backgroundColor = colorInput.value;
|
||||
}
|
||||
colorInputContainer.style.backgroundColor = colorInput.value;
|
||||
}
|
||||
}
|
||||
</script>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="startingNumber" th:text="#{addPageNumbers.selectText.4}"></label>
|
||||
<input type="number" class="form-control" id="startingNumber" name="startingNumber" min="1" required value="1">
|
||||
|
Loading…
Reference in New Issue
Block a user