html2canvas-pro
Version:
Screenshots with JavaScript. Next generation!
310 lines • 13.1 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", { value: true });
const assert_1 = require("assert");
const text_renderer_1 = require("../text-renderer");
const context_1 = require("../../../core/context");
const bounds_1 = require("../../../css/layout/bounds");
const text_1 = require("../../../css/layout/text");
const config_1 = require("../../../config");
const createMockContext = () => {
const mockWindow = {
document: {
createElement: (_name) => {
let _href = '';
return {
set href(value) {
_href = value;
},
get href() {
return _href;
},
get protocol() {
return 'http:';
},
get hostname() {
return 'localhost';
},
get port() {
return '';
}
};
}
},
location: { href: 'http://localhost/' }
};
const config = new config_1.Html2CanvasConfig({ window: mockWindow });
return new context_1.Context({
logging: false,
imageTimeout: 15000,
useCORS: false,
allowTaint: false
}, new bounds_1.Bounds(0, 0, 800, 600), config);
};
describe('TextRenderer', () => {
it('should be instantiated', () => {
const ctx = {
fillStyle: '',
font: '',
save: () => { },
restore: () => { }
};
const deps = {
ctx,
context: createMockContext(),
options: { scale: 1 }
};
const renderer = new text_renderer_1.TextRenderer(deps);
(0, assert_1.ok)(renderer);
// Test public methods exist
(0, assert_1.strictEqual)(typeof renderer.renderTextNode, 'function');
(0, assert_1.strictEqual)(typeof renderer.renderTextWithLetterSpacing, 'function');
(0, assert_1.strictEqual)(typeof renderer.createFontStyle, 'function');
});
});
describe('hasCJKCharacters', () => {
it('should return true for Chinese characters', () => {
(0, assert_1.strictEqual)((0, text_renderer_1.hasCJKCharacters)('快照'), true);
(0, assert_1.strictEqual)((0, text_renderer_1.hasCJKCharacters)('截图'), true);
(0, assert_1.strictEqual)((0, text_renderer_1.hasCJKCharacters)('中文'), true);
});
it('should return true for Japanese characters', () => {
(0, assert_1.strictEqual)((0, text_renderer_1.hasCJKCharacters)('ひらがな'), true); // Hiragana
(0, assert_1.strictEqual)((0, text_renderer_1.hasCJKCharacters)('カタカナ'), true); // Katakana
(0, assert_1.strictEqual)((0, text_renderer_1.hasCJKCharacters)('漢字'), true); // Kanji
});
it('should return true for Korean characters', () => {
(0, assert_1.strictEqual)((0, text_renderer_1.hasCJKCharacters)('한글'), true);
});
it('should return true for CJK punctuation and symbols', () => {
(0, assert_1.strictEqual)((0, text_renderer_1.hasCJKCharacters)('。'), true); // CJK full stop (U+3002)
(0, assert_1.strictEqual)((0, text_renderer_1.hasCJKCharacters)('、'), true); // CJK comma (U+3001)
(0, assert_1.strictEqual)((0, text_renderer_1.hasCJKCharacters)('「」'), true); // CJK brackets
});
it('should return true for fullwidth characters (U+FF01–U+FFEF)', () => {
(0, assert_1.strictEqual)((0, text_renderer_1.hasCJKCharacters)('!'), true); // Fullwidth ! (U+FF01, range start)
(0, assert_1.strictEqual)((0, text_renderer_1.hasCJKCharacters)('A'), true); // Fullwidth A (U+FF21)
(0, assert_1.strictEqual)((0, text_renderer_1.hasCJKCharacters)('1'), true); // Fullwidth 1 (U+FF11)
});
it('should return false for Latin characters', () => {
(0, assert_1.strictEqual)((0, text_renderer_1.hasCJKCharacters)('Hello'), false);
(0, assert_1.strictEqual)((0, text_renderer_1.hasCJKCharacters)('SOS'), false);
(0, assert_1.strictEqual)((0, text_renderer_1.hasCJKCharacters)('abc123'), false);
});
it('should return false for empty string', () => {
(0, assert_1.strictEqual)((0, text_renderer_1.hasCJKCharacters)(''), false);
});
it('should return true for mixed text containing CJK', () => {
(0, assert_1.strictEqual)((0, text_renderer_1.hasCJKCharacters)('SOS 快照'), true);
});
});
describe('renderTextWithLetterSpacing', () => {
it('should apply letterSpacing to each character x position (Issue #73 Bug1)', () => {
const fillCalls = [];
const measureResults = { A: 10, B: 12, C: 8 };
const ctx = {
fillStyle: '',
font: '',
textBaseline: 'alphabetic',
fillText(text, x, y) {
fillCalls.push({ text, x, y });
},
measureText(text) {
return { width: measureResults[text] ?? 10 };
}
};
const deps = {
ctx,
context: createMockContext(),
options: { scale: 1 }
};
const renderer = new text_renderer_1.TextRenderer(deps);
const bounds = new bounds_1.Bounds(100, 50, 200, 25);
const text = new text_1.TextBounds('ABC', bounds);
const letterSpacing = 5;
const baseline = 20;
renderer.renderTextWithLetterSpacing(text, letterSpacing, baseline);
// Verify letter spacing is added between characters
// A: x=100, B: x=100+10+5=115, C: x=115+12+5=132
(0, assert_1.strictEqual)(fillCalls.length, 3);
(0, assert_1.strictEqual)(fillCalls[0].text, 'A');
(0, assert_1.strictEqual)(fillCalls[0].x, 100);
(0, assert_1.strictEqual)(fillCalls[1].text, 'B');
(0, assert_1.strictEqual)(fillCalls[1].x, 115); // 100 + measureText('A').width(10) + letterSpacing(5)
(0, assert_1.strictEqual)(fillCalls[2].text, 'C');
(0, assert_1.strictEqual)(fillCalls[2].x, 132); // 115 + measureText('B').width(12) + letterSpacing(5)
// Verify y position uses baseline
fillCalls.forEach((call) => {
(0, assert_1.strictEqual)(call.y, bounds.top + baseline); // 50 + 20 = 70
});
});
it('should use ideographic baseline for CJK characters (Issue #73 Bug2)', () => {
const baselineChanges = [];
let currentBaseline = 'alphabetic';
const ctx = {
fillStyle: '',
font: '',
get textBaseline() {
return currentBaseline;
},
set textBaseline(value) {
currentBaseline = value;
baselineChanges.push(value);
},
fillText(_text, _x, _y) { },
measureText(_text) {
return { width: 25 };
}
};
const deps = {
ctx,
context: createMockContext(),
options: { scale: 1 }
};
const renderer = new text_renderer_1.TextRenderer(deps);
const bounds = new bounds_1.Bounds(0, 0, 100, 25);
const text = new text_1.TextBounds('快照', bounds);
renderer.renderTextWithLetterSpacing(text, 10, 20);
// Should have switched to ideographic for each CJK char and restored
// Pattern: [ideographic, alphabetic, ideographic, alphabetic]
(0, assert_1.ok)(baselineChanges.includes('ideographic'), 'should switch to ideographic baseline for CJK');
// Should restore alphabetic after each CJK char
const ideographicIdx = baselineChanges.indexOf('ideographic');
(0, assert_1.strictEqual)(baselineChanges[ideographicIdx + 1], 'alphabetic');
});
it('should not change textBaseline for non-CJK characters', () => {
const baselineChanges = [];
let currentBaseline = 'alphabetic';
const ctx = {
fillStyle: '',
font: '',
get textBaseline() {
return currentBaseline;
},
set textBaseline(value) {
currentBaseline = value;
baselineChanges.push(value);
},
fillText(_text, _x, _y) { },
measureText(_text) {
return { width: 10 };
}
};
const deps = {
ctx,
context: createMockContext(),
options: { scale: 1 }
};
const renderer = new text_renderer_1.TextRenderer(deps);
const bounds = new bounds_1.Bounds(0, 0, 100, 25);
const text = new text_1.TextBounds('ABC', bounds);
renderer.renderTextWithLetterSpacing(text, 5, 20);
// Should not switch to ideographic for Latin characters
(0, assert_1.ok)(!baselineChanges.includes('ideographic'), 'should not switch to ideographic for Latin text');
});
it('should render whole string in one call when letterSpacing is 0', () => {
const fillCalls = [];
const ctx = {
fillStyle: '',
font: '',
textBaseline: 'alphabetic',
fillText(text, x, y) {
fillCalls.push({ text, x, y });
},
measureText(_text) {
return { width: 30 };
}
};
const deps = {
ctx,
context: createMockContext(),
options: { scale: 1 }
};
const renderer = new text_renderer_1.TextRenderer(deps);
const bounds = new bounds_1.Bounds(10, 20, 100, 25);
const text = new text_1.TextBounds('Hello', bounds);
renderer.renderTextWithLetterSpacing(text, 0, 22);
(0, assert_1.deepStrictEqual)(fillCalls, [{ text: 'Hello', x: 10, y: 42 }]);
});
it('should handle negative letterSpacing correctly', () => {
const fillCalls = [];
const ctx = {
fillStyle: '',
font: '',
textBaseline: 'alphabetic',
fillText(text, x, _y) {
fillCalls.push({ text, x });
},
measureText(_text) {
return { width: 10 };
}
};
const deps = {
ctx,
context: createMockContext(),
options: { scale: 1 }
};
const renderer = new text_renderer_1.TextRenderer(deps);
const bounds = new bounds_1.Bounds(100, 0, 50, 20);
const text = new text_1.TextBounds('AB', bounds);
renderer.renderTextWithLetterSpacing(text, -3, 15);
// A: x=100, B: x=100 + 10 + (-3) = 107
(0, assert_1.strictEqual)(fillCalls[0].x, 100);
(0, assert_1.strictEqual)(fillCalls[1].x, 107);
});
it('should handle mixed CJK and Latin text with correct baseline per character', () => {
const baselineAtRender = {};
let currentBaseline = 'alphabetic';
const ctx = {
fillStyle: '',
font: '',
get textBaseline() {
return currentBaseline;
},
set textBaseline(value) {
currentBaseline = value;
},
fillText(text, _x, _y) {
baselineAtRender[text] = currentBaseline;
},
measureText(_text) {
return { width: 12 };
}
};
const deps = {
ctx,
context: createMockContext(),
options: { scale: 1 }
};
const renderer = new text_renderer_1.TextRenderer(deps);
const bounds = new bounds_1.Bounds(0, 0, 200, 25);
// Mixed: Latin 'A', CJK '快', Latin 'B'
const text = new text_1.TextBounds('A快B', bounds);
renderer.renderTextWithLetterSpacing(text, 5, 20);
(0, assert_1.strictEqual)(baselineAtRender['A'], 'alphabetic', 'Latin char should use alphabetic baseline');
(0, assert_1.strictEqual)(baselineAtRender['快'], 'ideographic', 'CJK char should use ideographic baseline');
(0, assert_1.strictEqual)(baselineAtRender['B'], 'alphabetic', 'Latin char after CJK should restore alphabetic baseline');
});
it('should handle empty string without errors', () => {
const ctx = {
fillStyle: '',
font: '',
textBaseline: 'alphabetic',
fillText(_text, _x, _y) { },
measureText(_text) {
return { width: 0 };
}
};
const deps = {
ctx,
context: createMockContext(),
options: { scale: 1 }
};
const renderer = new text_renderer_1.TextRenderer(deps);
const bounds = new bounds_1.Bounds(0, 0, 100, 25);
const text = new text_1.TextBounds('', bounds);
// Should not throw
renderer.renderTextWithLetterSpacing(text, 5, 20);
renderer.renderTextWithLetterSpacing(text, 0, 20);
});
});
//# sourceMappingURL=text-renderer.test.js.map