Merge pull request #782 from kekingcn/codex/fix-custom-watermark-settings

fix: 修复 PDF/Word 自定义水印设置不生效
This commit is contained in:
kl
2026-08-25 09:58:47 +08:00
committed by GitHub
4 changed files with 160 additions and 39 deletions

View File

@@ -1,5 +1,18 @@
var kkhighlightAll;
var watermarkTxt;
var watermarkSettings = {
start_x: 80,
start_y: 80,
x_space: 80,
y_space: 80,
color: 'black',
alpha: 0.2,
fontsize: '18px',
font: '微软雅黑',
width: 200,
height: 80,
angle: 30
};
const queryString = document.location.search.substring(1);
const params = (0, parseQueryString)(queryString);
@@ -13,6 +26,30 @@ if (kkpdfAutoFetch == "true") {
function isNotEmpty(value) {
return value !== null && value !== undefined && value !== '' && value !== 'false' ;
}
function getWatermarkStringParam(params, name, fallback) {
const value = params.get(name);
return isNotEmpty(value) ? value : fallback;
}
function getWatermarkNumberParam(params, name, fallback, isValid) {
const rawValue = params.get(name);
if (!isNotEmpty(rawValue)) return fallback;
const value = Number(rawValue);
return Number.isFinite(value) && isValid(value) ? value : fallback;
}
function configureWatermark(params) {
watermarkSettings.x_space = getWatermarkNumberParam(params, "watermarkxspace", watermarkSettings.x_space, value => value >= 0);
watermarkSettings.y_space = getWatermarkNumberParam(params, "watermarkyspace", watermarkSettings.y_space, value => value >= 0);
watermarkSettings.font = getWatermarkStringParam(params, "watermarkfont", watermarkSettings.font);
watermarkSettings.fontsize = getWatermarkStringParam(params, "watermarkfontsize", watermarkSettings.fontsize);
watermarkSettings.color = getWatermarkStringParam(params, "watermarkcolor", watermarkSettings.color);
watermarkSettings.alpha = getWatermarkNumberParam(params, "watermarkalpha", watermarkSettings.alpha, value => value >= 0 && value <= 1);
watermarkSettings.width = getWatermarkNumberParam(params, "watermarkwidth", watermarkSettings.width, value => value > 0);
watermarkSettings.height = getWatermarkNumberParam(params, "watermarkheight", watermarkSettings.height, value => value > 0);
watermarkSettings.angle = getWatermarkNumberParam(params, "watermarkangle", watermarkSettings.angle, Number.isFinite);
}
/**
* 通用水印生成函数
* @param {HTMLElement} container - 水印容器(相对定位的父元素)
@@ -23,20 +60,7 @@ function isNotEmpty(value) {
function addWatermark(container, watermarkTxt, explicitWidth = null, explicitHeight = null) {
if (!isNotEmpty(watermarkTxt)) return;
// 公共配置
const settings = {
start_x: 80,
start_y: 80,
x_space: 80,
y_space: 80,
color: 'black',
alpha: 0.2,
fontsize: '18px',
font: '微软雅黑',
width: 200,
height: 80,
angle: 30
};
const settings = watermarkSettings;
// 确定实际使用的宽高
let pageWidth, pageHeight;
@@ -55,30 +79,30 @@ function addWatermark(container, watermarkTxt, explicitWidth = null, explicitHei
maxY = Math.max(maxY, 250);
const fragment = document.createDocumentFragment();
for (let x = settings.start_x; x < maxX; x += settings.x_space) {
for (let y = settings.start_y; y < maxY; y += settings.y_space) {
const xStep = settings.width + settings.x_space;
const yStep = settings.height + settings.y_space;
for (let x = settings.start_x; x < maxX; x += xStep) {
for (let y = settings.start_y; y < maxY; y += yStep) {
const div = document.createElement('div');
div.className = 'mask_div';
div.appendChild(document.createTextNode(watermarkTxt));
div.style.cssText = `
filter: progid:DXImageTransform.Microsoft.Alpha(opacity=${settings.alpha * 100});
transform: rotate(-${settings.angle}deg);
visibility: visible;
position: absolute;
left: ${x}px;
top: ${y}px;
overflow: hidden;
z-index: 100;
pointer-events: none;
opacity: ${settings.alpha};
font-size: ${settings.fontsize};
font-family: ${settings.font};
color: ${settings.color};
text-align: center;
width: ${settings.width}px;
height: ${settings.height}px;
display: block;
`;
div.style.filter = `progid:DXImageTransform.Microsoft.Alpha(opacity=${settings.alpha * 100})`;
div.style.transform = `rotate(-${settings.angle}deg)`;
div.style.visibility = 'visible';
div.style.position = 'absolute';
div.style.left = `${x}px`;
div.style.top = `${y}px`;
div.style.overflow = 'hidden';
div.style.zIndex = '100';
div.style.pointerEvents = 'none';
div.style.opacity = settings.alpha;
div.style.fontSize = settings.fontsize;
div.style.fontFamily = settings.font;
div.style.color = settings.color;
div.style.textAlign = 'center';
div.style.width = `${settings.width}px`;
div.style.height = `${settings.height}px`;
div.style.display = 'block';
fragment.appendChild(div);
}
}
@@ -22069,7 +22093,8 @@ const PDFViewerApplication = {
disableBookmark = params.get("disablebookmark") ?? 'false';
disableEditing = params.get("disableediting") ?? 'false';
kkhighlightAll = params.get("pdfhighlightall") ?? 'false';
watermarkTxt= params.get('watermarktxt') ?? 'false';
watermarkTxt = params.get('watermarktxt') ?? 'false';
configureWatermark(params);
try {
file = new URL(file).href;
} catch {
@@ -24017,4 +24042,4 @@ if (document.readyState === "interactive" || document.readyState === "complete")
export { PDFViewerApplication, AppConstants as PDFViewerApplicationConstants, AppOptions as PDFViewerApplicationOptions };
//# sourceMappingURL=viewer.mjs.map
//# sourceMappingURL=viewer.mjs.map

View File

@@ -53,7 +53,18 @@
url = baseUrl + 'getCorsFile?urlPath=' + encodeURIComponent(Base64.encode(url)) + "&key=${kkkey}";
}
var viewerUrl = baseUrl + "pdfjs/web/viewer.html?file=" + encodeURIComponent(url);
var watermarkEncoded = encodeURIComponent('${watermarkTxt?js_string}');
var watermarkParams = {
watermarktxt: '${watermarkTxt?js_string}',
watermarkxspace: '${watermarkXSpace?js_string}',
watermarkyspace: '${watermarkYSpace?js_string}',
watermarkfont: '${watermarkFont?js_string}',
watermarkfontsize: '${watermarkFontsize?js_string}',
watermarkcolor: '${watermarkColor?js_string}',
watermarkalpha: '${watermarkAlpha?js_string}',
watermarkwidth: '${watermarkWidth?js_string}',
watermarkheight: '${watermarkHeight?js_string}',
watermarkangle: '${watermarkAngle?js_string}'
};
var highlightEncoded = encodeURIComponent('${highlightall?js_string}');
viewerUrl += "&disablepresentationmode=${pdfPresentationModeDisable}";
viewerUrl += "&disableopenfile=${pdfOpenFileDisable}";
@@ -61,7 +72,9 @@
viewerUrl += "&disabledownload=${pdfDownloadDisable}";
viewerUrl += "&disablebookmark=${pdfBookmarkDisable}";
viewerUrl += "&disableediting=${pdfDisableEditing}";
viewerUrl += "&watermarktxt=" + watermarkEncoded;
Object.keys(watermarkParams).forEach(function (name) {
viewerUrl += "&" + name + "=" + encodeURIComponent(watermarkParams[name]);
});
viewerUrl += "&pdfhighlightall=" + highlightEncoded;
viewerUrl += "#page=${page}"; // ?c 确保数字不包含千位分隔符
<#if "true" == pdfSidebarOpen>

View File

@@ -5,6 +5,7 @@ import org.junit.jupiter.api.Test;
import java.io.IOException;
import java.io.InputStream;
import java.nio.charset.StandardCharsets;
import java.util.Map;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertTrue;
@@ -35,6 +36,38 @@ public class PdfViewerCompatibilityTests {
assertTrue(pdfTemplate.contains("viewerUrl += \"&pagemode=none\";"));
}
@Test
void shouldForwardAndApplyAllPdfWatermarkSettings() throws IOException {
String pdfTemplate = readResource("/web/pdf.ftl");
String viewerScript = readResource("/static/pdfjs/web/viewer.mjs");
Map<String, String> watermarkParams = Map.ofEntries(
Map.entry("watermarktxt", "watermarkTxt"),
Map.entry("watermarkxspace", "watermarkXSpace"),
Map.entry("watermarkyspace", "watermarkYSpace"),
Map.entry("watermarkfont", "watermarkFont"),
Map.entry("watermarkfontsize", "watermarkFontsize"),
Map.entry("watermarkcolor", "watermarkColor"),
Map.entry("watermarkalpha", "watermarkAlpha"),
Map.entry("watermarkwidth", "watermarkWidth"),
Map.entry("watermarkheight", "watermarkHeight"),
Map.entry("watermarkangle", "watermarkAngle")
);
watermarkParams.forEach((queryParam, templateAttribute) -> {
assertTrue(pdfTemplate.contains(queryParam + ": '${" + templateAttribute + "?js_string}'"),
() -> "PDF template does not forward " + templateAttribute);
assertTrue(viewerScript.contains("\"" + queryParam + "\"")
|| viewerScript.contains("'" + queryParam + "'"),
() -> "PDF viewer does not consume " + queryParam);
});
assertTrue(viewerScript.contains("div.style.fontFamily = settings.font;"));
assertTrue(viewerScript.contains("div.style.fontSize = settings.fontsize;"));
assertTrue(viewerScript.contains("div.style.color = settings.color;"));
assertTrue(viewerScript.contains("div.style.opacity = settings.alpha;"));
assertTrue(viewerScript.contains("const xStep = settings.width + settings.x_space;"));
assertTrue(viewerScript.contains("const yStep = settings.height + settings.y_space;"));
}
@Test
void shouldPreferPdfForOfficePreviewByDefault() throws IOException {
String properties = readResource("/application.properties");

View File

@@ -172,3 +172,53 @@ test('21 security: block 10.x host in getCorsFile', async ({ request }) => {
const body = await resp.text();
expect(body).toContain('不受信任');
});
test('22 pdf preview applies custom watermark settings', async ({ page }) => {
const query = new URLSearchParams({
url: b64(`${fixtureBase}/sample.pdf`),
watermarkTxt: 'Custom watermark',
watermarkXSpace: '37',
watermarkYSpace: '43',
watermarkFont: 'Courier New',
watermarkFontsize: '31px',
watermarkColor: 'rgb(1, 2, 3)',
watermarkAlpha: '0.65',
watermarkWidth: '100',
watermarkHeight: '77',
watermarkAngle: '17',
});
await page.goto(`/onlinePreview?${query.toString()}`);
const watermark = page.frameLocator('#pdfFrame').locator('.mask_div').first();
await expect(watermark).toHaveText('Custom watermark');
const style = await watermark.evaluate(element => ({
fontFamily: element.style.fontFamily,
fontSize: element.style.fontSize,
color: element.style.color,
opacity: element.style.opacity,
width: element.style.width,
height: element.style.height,
transform: element.style.transform,
}));
expect(style).toEqual({
fontFamily: '"Courier New"',
fontSize: '31px',
color: 'rgb(1, 2, 3)',
opacity: '0.65',
width: '100px',
height: '77px',
transform: 'rotate(-17deg)',
});
const positions = await page.frameLocator('#pdfFrame').locator('.mask_div').evaluateAll(elements =>
elements.map(element => ({
left: Number.parseFloat(element.style.left),
top: Number.parseFloat(element.style.top),
})),
);
const nextColumn = positions.find(position => position.left !== positions[0].left);
expect(positions[1].top - positions[0].top).toBe(120);
expect(nextColumn).toBeDefined();
expect(nextColumn!.left - positions[0].left).toBe(137);
});