chess2img 0.3.0 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +79 -9
- package/dist/index.cjs +479 -132
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +12 -1
- package/dist/index.d.ts +12 -1
- package/dist/index.js +471 -129
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.cjs
CHANGED
|
@@ -27,7 +27,12 @@ __export(index_exports, {
|
|
|
27
27
|
ThemeError: () => ThemeError,
|
|
28
28
|
ValidationError: () => ValidationError,
|
|
29
29
|
registerTheme: () => registerTheme,
|
|
30
|
-
renderChess: () => renderChess
|
|
30
|
+
renderChess: () => renderChess,
|
|
31
|
+
renderFile: () => renderFile,
|
|
32
|
+
renderJpeg: () => renderJpeg,
|
|
33
|
+
renderJpegFile: () => renderJpegFile,
|
|
34
|
+
renderSvg: () => renderSvg,
|
|
35
|
+
renderSvgFile: () => renderSvgFile
|
|
31
36
|
});
|
|
32
37
|
module.exports = __toCommonJS(index_exports);
|
|
33
38
|
|
|
@@ -63,23 +68,6 @@ var IOError = class extends Error {
|
|
|
63
68
|
}
|
|
64
69
|
};
|
|
65
70
|
|
|
66
|
-
// src/core/parsers.ts
|
|
67
|
-
var import_chess = require("chess.js");
|
|
68
|
-
|
|
69
|
-
// src/core/board.ts
|
|
70
|
-
var FILES = ["a", "b", "c", "d", "e", "f", "g", "h"];
|
|
71
|
-
var RANKS = ["8", "7", "6", "5", "4", "3", "2", "1"];
|
|
72
|
-
var SQUARES = RANKS.flatMap(
|
|
73
|
-
(rank) => FILES.map((file) => `${file}${rank}`)
|
|
74
|
-
);
|
|
75
|
-
function createEmptyBoardPosition() {
|
|
76
|
-
return {
|
|
77
|
-
squares: Object.fromEntries(
|
|
78
|
-
SQUARES.map((square) => [square, null])
|
|
79
|
-
)
|
|
80
|
-
};
|
|
81
|
-
}
|
|
82
|
-
|
|
83
71
|
// src/core/validators.ts
|
|
84
72
|
var import_canvas = require("canvas");
|
|
85
73
|
var SQUARE_PATTERN = /^[a-h][1-8]$/;
|
|
@@ -249,6 +237,9 @@ function validateHighlightEntry(entry) {
|
|
|
249
237
|
if (entry.lineWidth !== void 0 && (!Number.isFinite(entry.lineWidth) || entry.lineWidth <= 0)) {
|
|
250
238
|
throw new ValidationError("highlight.lineWidth must be a finite number greater than 0");
|
|
251
239
|
}
|
|
240
|
+
if (entry.radius !== void 0 && (!Number.isFinite(entry.radius) || entry.radius <= 0 || entry.radius > 0.5)) {
|
|
241
|
+
throw new ValidationError("highlight.radius must be a finite number greater than 0 and at most 0.5");
|
|
242
|
+
}
|
|
252
243
|
}
|
|
253
244
|
function validateHighlightOptions(highlights) {
|
|
254
245
|
if (highlights === void 0) {
|
|
@@ -269,71 +260,23 @@ function validateHighlightsInput(highlights, highlightSquares) {
|
|
|
269
260
|
validateHighlightOptions(highlightSquares);
|
|
270
261
|
}
|
|
271
262
|
|
|
272
|
-
// src/core/parsers.ts
|
|
273
|
-
var PIECE_SYMBOL_TO_KEY = {
|
|
274
|
-
K: "wK",
|
|
275
|
-
Q: "wQ",
|
|
276
|
-
R: "wR",
|
|
277
|
-
B: "wB",
|
|
278
|
-
N: "wN",
|
|
279
|
-
P: "wP",
|
|
280
|
-
k: "bK",
|
|
281
|
-
q: "bQ",
|
|
282
|
-
r: "bR",
|
|
283
|
-
b: "bB",
|
|
284
|
-
n: "bN",
|
|
285
|
-
p: "bP"
|
|
286
|
-
};
|
|
287
|
-
function chessBoardToBoardArray(board) {
|
|
288
|
-
return board.map(
|
|
289
|
-
(rank) => rank.map((piece) => {
|
|
290
|
-
if (!piece) {
|
|
291
|
-
return null;
|
|
292
|
-
}
|
|
293
|
-
return piece.color === "w" ? piece.type.toUpperCase() : piece.type;
|
|
294
|
-
})
|
|
295
|
-
);
|
|
296
|
-
}
|
|
297
|
-
function parseFEN(fen) {
|
|
298
|
-
const chess = new import_chess.Chess();
|
|
299
|
-
try {
|
|
300
|
-
chess.load(fen);
|
|
301
|
-
} catch (error) {
|
|
302
|
-
throw new ParseError("Invalid FEN", { cause: error });
|
|
303
|
-
}
|
|
304
|
-
return parseBoardArray(chessBoardToBoardArray(chess.board()));
|
|
305
|
-
}
|
|
306
|
-
function parsePGN(pgn) {
|
|
307
|
-
const chess = new import_chess.Chess();
|
|
308
|
-
try {
|
|
309
|
-
chess.loadPgn(pgn);
|
|
310
|
-
} catch (error) {
|
|
311
|
-
throw new ParseError("Invalid PGN", { cause: error });
|
|
312
|
-
}
|
|
313
|
-
return parseBoardArray(chessBoardToBoardArray(chess.board()));
|
|
314
|
-
}
|
|
315
|
-
function parseBoardArray(board) {
|
|
316
|
-
const validatedBoard = validateBoardArray(board);
|
|
317
|
-
const position = createEmptyBoardPosition();
|
|
318
|
-
validatedBoard.forEach((rank, rankIndex) => {
|
|
319
|
-
rank.forEach((cell, fileIndex) => {
|
|
320
|
-
if (cell === null) {
|
|
321
|
-
return;
|
|
322
|
-
}
|
|
323
|
-
const pieceKey = PIECE_SYMBOL_TO_KEY[cell];
|
|
324
|
-
if (!pieceKey) {
|
|
325
|
-
throw new ValidationError(`Invalid board piece: ${cell}`);
|
|
326
|
-
}
|
|
327
|
-
const square = `${FILES[fileIndex]}${8 - rankIndex}`;
|
|
328
|
-
position.squares[square] = pieceKey;
|
|
329
|
-
});
|
|
330
|
-
});
|
|
331
|
-
return position;
|
|
332
|
-
}
|
|
333
|
-
|
|
334
263
|
// src/render/canvas-renderer.ts
|
|
335
264
|
var import_canvas3 = require("canvas");
|
|
336
265
|
|
|
266
|
+
// src/core/board.ts
|
|
267
|
+
var FILES = ["a", "b", "c", "d", "e", "f", "g", "h"];
|
|
268
|
+
var RANKS = ["8", "7", "6", "5", "4", "3", "2", "1"];
|
|
269
|
+
var SQUARES = RANKS.flatMap(
|
|
270
|
+
(rank) => FILES.map((file) => `${file}${rank}`)
|
|
271
|
+
);
|
|
272
|
+
function createEmptyBoardPosition() {
|
|
273
|
+
return {
|
|
274
|
+
squares: Object.fromEntries(
|
|
275
|
+
SQUARES.map((square) => [square, null])
|
|
276
|
+
)
|
|
277
|
+
};
|
|
278
|
+
}
|
|
279
|
+
|
|
337
280
|
// src/core/geometry.ts
|
|
338
281
|
function createBoardGeometry({
|
|
339
282
|
size,
|
|
@@ -569,6 +512,10 @@ function resolveCircleLineWidth(squareSize, lineWidth) {
|
|
|
569
512
|
const candidate = lineWidth ?? squareSize * 0.08;
|
|
570
513
|
return Math.max(2, Math.min(8, candidate));
|
|
571
514
|
}
|
|
515
|
+
function resolveCircleRadius(squareSize, radius, lineWidth) {
|
|
516
|
+
const radiusPx = squareSize * (radius ?? 0.42);
|
|
517
|
+
return Math.max(0, radiusPx - lineWidth / 2);
|
|
518
|
+
}
|
|
572
519
|
function resolveBorderCoordinateFontSize(context, geometry) {
|
|
573
520
|
const maxFontSize = Math.floor(
|
|
574
521
|
Math.min(geometry.squareSize * 0.6, geometry.borderSize * 0.65)
|
|
@@ -718,11 +665,16 @@ function drawCircleHighlights(context, request, geometry) {
|
|
|
718
665
|
squareGeometry.size,
|
|
719
666
|
highlight.lineWidth
|
|
720
667
|
);
|
|
668
|
+
const radius = resolveCircleRadius(
|
|
669
|
+
squareGeometry.size,
|
|
670
|
+
highlight.radius,
|
|
671
|
+
context.lineWidth
|
|
672
|
+
);
|
|
721
673
|
context.beginPath();
|
|
722
674
|
context.arc(
|
|
723
675
|
centerX,
|
|
724
676
|
centerY,
|
|
725
|
-
|
|
677
|
+
radius,
|
|
726
678
|
0,
|
|
727
679
|
Math.PI * 2
|
|
728
680
|
);
|
|
@@ -753,6 +705,9 @@ async function drawPieces(context, request, geometry) {
|
|
|
753
705
|
}
|
|
754
706
|
}
|
|
755
707
|
var CanvasPngRenderer = class {
|
|
708
|
+
createOutputBuffer(canvas) {
|
|
709
|
+
return canvas.toBuffer("image/png");
|
|
710
|
+
}
|
|
756
711
|
async render(request) {
|
|
757
712
|
try {
|
|
758
713
|
const geometry = createBoardGeometry({
|
|
@@ -770,7 +725,7 @@ var CanvasPngRenderer = class {
|
|
|
770
725
|
drawCircleHighlights(context, request, geometry);
|
|
771
726
|
drawCoordinates(context, request, geometry);
|
|
772
727
|
await drawPieces(context, request, geometry);
|
|
773
|
-
return
|
|
728
|
+
return this.createOutputBuffer(canvas);
|
|
774
729
|
} catch (error) {
|
|
775
730
|
if (error instanceof RenderError) {
|
|
776
731
|
throw error;
|
|
@@ -780,11 +735,282 @@ var CanvasPngRenderer = class {
|
|
|
780
735
|
}
|
|
781
736
|
};
|
|
782
737
|
|
|
783
|
-
// src/
|
|
738
|
+
// src/render/canvas-jpeg-renderer.ts
|
|
739
|
+
var import_canvas4 = require("canvas");
|
|
740
|
+
var CanvasJpegRenderer = class extends CanvasPngRenderer {
|
|
741
|
+
createOutputBuffer(canvas) {
|
|
742
|
+
return canvas.toBuffer("image/jpeg");
|
|
743
|
+
}
|
|
744
|
+
};
|
|
745
|
+
|
|
746
|
+
// src/render/svg-renderer.ts
|
|
784
747
|
var import_promises2 = require("fs/promises");
|
|
748
|
+
var import_canvas5 = require("canvas");
|
|
749
|
+
var svgSourceCache2 = new SourceAssetCache();
|
|
750
|
+
var binarySourceCache = new SourceAssetCache();
|
|
751
|
+
var measureContext = (0, import_canvas5.createCanvas)(1, 1).getContext("2d");
|
|
752
|
+
var MIN_COORDINATE_FONT_SIZE2 = 8;
|
|
753
|
+
var MAX_FILE_LABEL_WIDTH_RATIO2 = 0.75;
|
|
754
|
+
var MAX_RANK_LABEL_WIDTH_RATIO2 = 0.7;
|
|
755
|
+
var MAX_LABEL_HEIGHT_RATIO2 = 0.7;
|
|
756
|
+
var INSIDE_COORDINATE_MAX_FONT_RATIO2 = 0.34;
|
|
757
|
+
var INSIDE_LIGHT_LABEL_COLOR2 = "rgba(255,255,255,0.6)";
|
|
758
|
+
var INSIDE_DARK_LABEL_COLOR2 = "rgba(0,0,0,0.45)";
|
|
759
|
+
function escapeXml(value) {
|
|
760
|
+
return value.replaceAll("&", "&").replaceAll("<", "<").replaceAll(">", ">").replaceAll('"', """).replaceAll("'", "'");
|
|
761
|
+
}
|
|
762
|
+
function isDarkSquare2(square) {
|
|
763
|
+
const fileIndex = square.charCodeAt(0) - 97;
|
|
764
|
+
const rankNumber = Number(square[1]);
|
|
765
|
+
return (fileIndex + rankNumber) % 2 === 1;
|
|
766
|
+
}
|
|
767
|
+
function resolveInsideLabelColor2(request, square) {
|
|
768
|
+
if (request.coordinates.color) {
|
|
769
|
+
return request.coordinates.color;
|
|
770
|
+
}
|
|
771
|
+
return isDarkSquare2(square) ? INSIDE_LIGHT_LABEL_COLOR2 : INSIDE_DARK_LABEL_COLOR2;
|
|
772
|
+
}
|
|
773
|
+
function resolveHighlightOpacity2(style, color, opacity) {
|
|
774
|
+
if (opacity !== void 0) {
|
|
775
|
+
return opacity;
|
|
776
|
+
}
|
|
777
|
+
if (style === "circle" || color !== void 0) {
|
|
778
|
+
return 0.9;
|
|
779
|
+
}
|
|
780
|
+
return 1;
|
|
781
|
+
}
|
|
782
|
+
function resolveCircleLineWidth2(squareSize, lineWidth) {
|
|
783
|
+
const candidate = lineWidth ?? squareSize * 0.08;
|
|
784
|
+
return Math.max(2, Math.min(8, candidate));
|
|
785
|
+
}
|
|
786
|
+
function resolveCircleRadius2(squareSize, radius, lineWidth) {
|
|
787
|
+
const radiusPx = squareSize * (radius ?? 0.42);
|
|
788
|
+
return Math.max(0, radiusPx - lineWidth / 2);
|
|
789
|
+
}
|
|
790
|
+
function resolveBorderCoordinateFontSize2(geometry) {
|
|
791
|
+
const maxFontSize = Math.floor(
|
|
792
|
+
Math.min(geometry.squareSize * 0.6, geometry.borderSize * 0.65)
|
|
793
|
+
);
|
|
794
|
+
for (let candidate = maxFontSize; candidate >= MIN_COORDINATE_FONT_SIZE2; candidate -= 1) {
|
|
795
|
+
measureContext.font = `${candidate}px sans-serif`;
|
|
796
|
+
const filesFit = geometry.borderFileLabels.every((label) => {
|
|
797
|
+
const metrics = measureContext.measureText(label.text);
|
|
798
|
+
const textHeight = metrics.actualBoundingBoxAscent + metrics.actualBoundingBoxDescent;
|
|
799
|
+
return metrics.width <= geometry.squareSize * MAX_FILE_LABEL_WIDTH_RATIO2 && textHeight <= geometry.borderSize * MAX_LABEL_HEIGHT_RATIO2;
|
|
800
|
+
});
|
|
801
|
+
const ranksFit = geometry.borderRankLabels.every((label) => {
|
|
802
|
+
const metrics = measureContext.measureText(label.text);
|
|
803
|
+
const textHeight = metrics.actualBoundingBoxAscent + metrics.actualBoundingBoxDescent;
|
|
804
|
+
return metrics.width <= geometry.borderSize * MAX_RANK_LABEL_WIDTH_RATIO2 && textHeight <= geometry.squareSize * MAX_LABEL_HEIGHT_RATIO2;
|
|
805
|
+
});
|
|
806
|
+
if (filesFit && ranksFit) {
|
|
807
|
+
return candidate;
|
|
808
|
+
}
|
|
809
|
+
}
|
|
810
|
+
return null;
|
|
811
|
+
}
|
|
812
|
+
function resolveInsideCoordinateFontSize2(geometry) {
|
|
813
|
+
const maxFontSize = Math.floor(
|
|
814
|
+
geometry.squareSize * INSIDE_COORDINATE_MAX_FONT_RATIO2
|
|
815
|
+
);
|
|
816
|
+
for (let candidate = maxFontSize; candidate >= MIN_COORDINATE_FONT_SIZE2; candidate -= 1) {
|
|
817
|
+
measureContext.font = `${candidate}px sans-serif`;
|
|
818
|
+
const filesFit = geometry.insideFileLabels.every((label) => {
|
|
819
|
+
const metrics = measureContext.measureText(label.text);
|
|
820
|
+
const textHeight = metrics.actualBoundingBoxAscent + metrics.actualBoundingBoxDescent;
|
|
821
|
+
return metrics.width <= geometry.insideLabelMaxWidth && textHeight <= geometry.insideLabelMaxHeight;
|
|
822
|
+
});
|
|
823
|
+
const ranksFit = geometry.insideRankLabels.every((label) => {
|
|
824
|
+
const metrics = measureContext.measureText(label.text);
|
|
825
|
+
const textHeight = metrics.actualBoundingBoxAscent + metrics.actualBoundingBoxDescent;
|
|
826
|
+
return metrics.width <= geometry.insideLabelMaxWidth && textHeight <= geometry.insideLabelMaxHeight;
|
|
827
|
+
});
|
|
828
|
+
if (filesFit && ranksFit) {
|
|
829
|
+
return candidate;
|
|
830
|
+
}
|
|
831
|
+
}
|
|
832
|
+
return null;
|
|
833
|
+
}
|
|
834
|
+
function textAnchor(value) {
|
|
835
|
+
if (value === "center") {
|
|
836
|
+
return "middle";
|
|
837
|
+
}
|
|
838
|
+
return value === "right" ? "end" : "start";
|
|
839
|
+
}
|
|
840
|
+
function dominantBaseline(value) {
|
|
841
|
+
if (value === "middle") {
|
|
842
|
+
return "middle";
|
|
843
|
+
}
|
|
844
|
+
return value === "bottom" ? "text-after-edge" : "hanging";
|
|
845
|
+
}
|
|
846
|
+
async function readSvgSource2(filePath) {
|
|
847
|
+
const cached = svgSourceCache2.get(filePath);
|
|
848
|
+
if (cached) {
|
|
849
|
+
return cached;
|
|
850
|
+
}
|
|
851
|
+
try {
|
|
852
|
+
const source = await (0, import_promises2.readFile)(filePath, "utf8");
|
|
853
|
+
svgSourceCache2.set(filePath, source);
|
|
854
|
+
return source;
|
|
855
|
+
} catch (error) {
|
|
856
|
+
throw new RenderError(`Failed to read SVG asset: ${filePath}`, { cause: error });
|
|
857
|
+
}
|
|
858
|
+
}
|
|
859
|
+
async function readBinarySource(filePath) {
|
|
860
|
+
const cached = binarySourceCache.get(filePath);
|
|
861
|
+
if (cached) {
|
|
862
|
+
return cached;
|
|
863
|
+
}
|
|
864
|
+
try {
|
|
865
|
+
const source = await (0, import_promises2.readFile)(filePath);
|
|
866
|
+
binarySourceCache.set(filePath, source);
|
|
867
|
+
return source;
|
|
868
|
+
} catch (error) {
|
|
869
|
+
throw new RenderError(`Failed to read image asset: ${filePath}`, { cause: error });
|
|
870
|
+
}
|
|
871
|
+
}
|
|
872
|
+
function stripSvgPreamble(source) {
|
|
873
|
+
return source.replace(/^\uFEFF/, "").replace(/<\?xml[\s\S]*?\?>/gi, "").replace(/<!doctype[\s\S]*?>/gi, "").trim();
|
|
874
|
+
}
|
|
875
|
+
function inlineSvgPiece(source, x, y, size) {
|
|
876
|
+
const sanitized = stripSvgPreamble(source);
|
|
877
|
+
if (!sanitized.startsWith("<svg")) {
|
|
878
|
+
throw new RenderError("Invalid SVG asset source");
|
|
879
|
+
}
|
|
880
|
+
return sanitized.replace(/<svg\b([^>]*)>/i, (_match, attrs) => {
|
|
881
|
+
const cleanedAttrs = attrs.replace(/\s(?:x|y|width|height)=(".*?"|'.*?'|[^\s>]+)/gi, "").trim();
|
|
882
|
+
const preservedAttrs = cleanedAttrs ? ` ${cleanedAttrs}` : "";
|
|
883
|
+
return `<svg x="${x}" y="${y}" width="${size}" height="${size}"${preservedAttrs}>`;
|
|
884
|
+
});
|
|
885
|
+
}
|
|
886
|
+
async function renderPieceElement(asset, x, y, size) {
|
|
887
|
+
if (asset.kind === "svg") {
|
|
888
|
+
const source = await readSvgSource2(asset.source);
|
|
889
|
+
return inlineSvgPiece(source, x, y, size);
|
|
890
|
+
}
|
|
891
|
+
const buffer = await readBinarySource(asset.source);
|
|
892
|
+
return [
|
|
893
|
+
`<image x="${x}" y="${y}" width="${size}" height="${size}"`,
|
|
894
|
+
` href="data:image/png;base64,${buffer.toString("base64")}" />`
|
|
895
|
+
].join("");
|
|
896
|
+
}
|
|
897
|
+
async function renderPieces(request, geometry) {
|
|
898
|
+
const pieces = [];
|
|
899
|
+
for (const square of SQUARES) {
|
|
900
|
+
const pieceKey = request.board.squares[square];
|
|
901
|
+
if (!pieceKey) {
|
|
902
|
+
continue;
|
|
903
|
+
}
|
|
904
|
+
const squareGeometry = geometry.squares[square];
|
|
905
|
+
pieces.push(
|
|
906
|
+
await renderPieceElement(
|
|
907
|
+
request.theme.pieces[pieceKey],
|
|
908
|
+
squareGeometry.x,
|
|
909
|
+
squareGeometry.y,
|
|
910
|
+
squareGeometry.size
|
|
911
|
+
)
|
|
912
|
+
);
|
|
913
|
+
}
|
|
914
|
+
return pieces;
|
|
915
|
+
}
|
|
916
|
+
function renderCoordinates(request, geometry) {
|
|
917
|
+
if (!request.coordinates.enabled) {
|
|
918
|
+
return [];
|
|
919
|
+
}
|
|
920
|
+
if (request.coordinates.position === "border") {
|
|
921
|
+
if (geometry.borderSize === 0) {
|
|
922
|
+
return [];
|
|
923
|
+
}
|
|
924
|
+
const fontSize2 = resolveBorderCoordinateFontSize2(geometry);
|
|
925
|
+
if (fontSize2 === null) {
|
|
926
|
+
return [];
|
|
927
|
+
}
|
|
928
|
+
return [
|
|
929
|
+
...geometry.borderFileLabels,
|
|
930
|
+
...geometry.borderRankLabels
|
|
931
|
+
].map(
|
|
932
|
+
(label) => `<text x="${label.x}" y="${label.y}" fill="${escapeXml(request.coordinates.color ?? "#333")}" font-family="sans-serif" font-size="${fontSize2}" text-anchor="middle" dominant-baseline="middle">${escapeXml(label.text)}</text>`
|
|
933
|
+
);
|
|
934
|
+
}
|
|
935
|
+
const fontSize = resolveInsideCoordinateFontSize2(geometry);
|
|
936
|
+
if (fontSize === null) {
|
|
937
|
+
return [];
|
|
938
|
+
}
|
|
939
|
+
return [
|
|
940
|
+
...geometry.insideFileLabels.map(
|
|
941
|
+
(label) => `<text x="${label.x}" y="${label.y}" fill="${escapeXml(resolveInsideLabelColor2(request, label.square))}" font-family="sans-serif" font-size="${fontSize}" text-anchor="${textAnchor(label.textAlign)}" dominant-baseline="${dominantBaseline(label.textBaseline)}">${escapeXml(label.text)}</text>`
|
|
942
|
+
),
|
|
943
|
+
...geometry.insideRankLabels.map(
|
|
944
|
+
(label) => `<text x="${label.x}" y="${label.y}" fill="${escapeXml(resolveInsideLabelColor2(request, label.square))}" font-family="sans-serif" font-size="${fontSize}" text-anchor="${textAnchor(label.textAlign)}" dominant-baseline="${dominantBaseline(label.textBaseline)}">${escapeXml(label.text)}</text>`
|
|
945
|
+
)
|
|
946
|
+
];
|
|
947
|
+
}
|
|
948
|
+
var SvgRenderer = class {
|
|
949
|
+
async render(request) {
|
|
950
|
+
try {
|
|
951
|
+
const geometry = createBoardGeometry({
|
|
952
|
+
size: request.size,
|
|
953
|
+
padding: request.padding,
|
|
954
|
+
borderSize: request.borderSize,
|
|
955
|
+
flipped: request.flipped
|
|
956
|
+
});
|
|
957
|
+
const elements = [
|
|
958
|
+
`<rect x="0" y="0" width="${geometry.imageWidth}" height="${geometry.imageHeight}" fill="${escapeXml(request.colors.lightSquare)}" />`
|
|
959
|
+
];
|
|
960
|
+
for (const square of SQUARES) {
|
|
961
|
+
const squareGeometry = geometry.squares[square];
|
|
962
|
+
elements.push(
|
|
963
|
+
`<rect x="${squareGeometry.x}" y="${squareGeometry.y}" width="${squareGeometry.size}" height="${squareGeometry.size}" fill="${escapeXml(isDarkSquare2(square) ? request.colors.darkSquare : request.colors.lightSquare)}" />`
|
|
964
|
+
);
|
|
965
|
+
}
|
|
966
|
+
for (const highlight of request.highlights) {
|
|
967
|
+
if (highlight.style !== "fill") {
|
|
968
|
+
continue;
|
|
969
|
+
}
|
|
970
|
+
const squareGeometry = geometry.squares[highlight.square];
|
|
971
|
+
elements.push(
|
|
972
|
+
`<rect x="${squareGeometry.x}" y="${squareGeometry.y}" width="${squareGeometry.size}" height="${squareGeometry.size}" fill="${escapeXml(highlight.color ?? request.colors.highlight)}" fill-opacity="${resolveHighlightOpacity2(highlight.style, highlight.color, highlight.opacity)}" />`
|
|
973
|
+
);
|
|
974
|
+
}
|
|
975
|
+
for (const highlight of request.highlights) {
|
|
976
|
+
if (highlight.style !== "circle") {
|
|
977
|
+
continue;
|
|
978
|
+
}
|
|
979
|
+
const squareGeometry = geometry.squares[highlight.square];
|
|
980
|
+
const lineWidth = resolveCircleLineWidth2(squareGeometry.size, highlight.lineWidth);
|
|
981
|
+
const radius = resolveCircleRadius2(squareGeometry.size, highlight.radius, lineWidth);
|
|
982
|
+
elements.push(
|
|
983
|
+
`<circle cx="${squareGeometry.x + squareGeometry.size / 2}" cy="${squareGeometry.y + squareGeometry.size / 2}" r="${radius}" fill="none" stroke="${escapeXml(highlight.color ?? "#ffcc00")}" stroke-width="${lineWidth}" stroke-opacity="${resolveHighlightOpacity2(highlight.style, highlight.color, highlight.opacity)}" />`
|
|
984
|
+
);
|
|
985
|
+
}
|
|
986
|
+
elements.push(...renderCoordinates(request, geometry));
|
|
987
|
+
elements.push(...await renderPieces(request, geometry));
|
|
988
|
+
return [
|
|
989
|
+
`<svg xmlns="http://www.w3.org/2000/svg" width="${geometry.imageWidth}" height="${geometry.imageHeight}" viewBox="0 0 ${geometry.imageWidth} ${geometry.imageHeight}">`,
|
|
990
|
+
...elements,
|
|
991
|
+
"</svg>"
|
|
992
|
+
].join("");
|
|
993
|
+
} catch (error) {
|
|
994
|
+
if (error instanceof RenderError) {
|
|
995
|
+
throw error;
|
|
996
|
+
}
|
|
997
|
+
throw new RenderError("Failed to render chess board as SVG", { cause: error });
|
|
998
|
+
}
|
|
999
|
+
}
|
|
1000
|
+
};
|
|
1001
|
+
|
|
1002
|
+
// src/utils/io.ts
|
|
1003
|
+
var import_promises3 = require("fs/promises");
|
|
785
1004
|
async function writeBufferToFile(filePath, buffer) {
|
|
786
1005
|
try {
|
|
787
|
-
await (0,
|
|
1006
|
+
await (0, import_promises3.writeFile)(filePath, buffer);
|
|
1007
|
+
} catch (error) {
|
|
1008
|
+
throw new IOError(`Failed to write file: ${filePath}`, { cause: error });
|
|
1009
|
+
}
|
|
1010
|
+
}
|
|
1011
|
+
async function writeStringToFile(filePath, contents) {
|
|
1012
|
+
try {
|
|
1013
|
+
await (0, import_promises3.writeFile)(filePath, contents, "utf8");
|
|
788
1014
|
} catch (error) {
|
|
789
1015
|
throw new IOError(`Failed to write file: ${filePath}`, { cause: error });
|
|
790
1016
|
}
|
|
@@ -796,7 +1022,11 @@ function normalizeHighlightEntries(input) {
|
|
|
796
1022
|
if (typeof entry === "string") {
|
|
797
1023
|
return {
|
|
798
1024
|
square: validateSquare(entry),
|
|
799
|
-
style: "fill"
|
|
1025
|
+
style: "fill",
|
|
1026
|
+
color: void 0,
|
|
1027
|
+
opacity: void 0,
|
|
1028
|
+
lineWidth: void 0,
|
|
1029
|
+
radius: void 0
|
|
800
1030
|
};
|
|
801
1031
|
}
|
|
802
1032
|
const style = entry.style ?? "fill";
|
|
@@ -805,7 +1035,8 @@ function normalizeHighlightEntries(input) {
|
|
|
805
1035
|
style,
|
|
806
1036
|
color: entry.color ?? (style === "circle" ? "#ffcc00" : void 0),
|
|
807
1037
|
opacity: entry.opacity ?? (style === "circle" ? 0.9 : void 0),
|
|
808
|
-
lineWidth: entry.lineWidth
|
|
1038
|
+
lineWidth: entry.lineWidth,
|
|
1039
|
+
radius: style === "circle" ? entry.radius ?? 0.42 : void 0
|
|
809
1040
|
};
|
|
810
1041
|
});
|
|
811
1042
|
}
|
|
@@ -1023,6 +1254,112 @@ function normalizeRenderInputs(options) {
|
|
|
1023
1254
|
};
|
|
1024
1255
|
}
|
|
1025
1256
|
|
|
1257
|
+
// src/core/parsers.ts
|
|
1258
|
+
var import_chess = require("chess.js");
|
|
1259
|
+
var PIECE_SYMBOL_TO_KEY = {
|
|
1260
|
+
K: "wK",
|
|
1261
|
+
Q: "wQ",
|
|
1262
|
+
R: "wR",
|
|
1263
|
+
B: "wB",
|
|
1264
|
+
N: "wN",
|
|
1265
|
+
P: "wP",
|
|
1266
|
+
k: "bK",
|
|
1267
|
+
q: "bQ",
|
|
1268
|
+
r: "bR",
|
|
1269
|
+
b: "bB",
|
|
1270
|
+
n: "bN",
|
|
1271
|
+
p: "bP"
|
|
1272
|
+
};
|
|
1273
|
+
function chessBoardToBoardArray(board) {
|
|
1274
|
+
return board.map(
|
|
1275
|
+
(rank) => rank.map((piece) => {
|
|
1276
|
+
if (!piece) {
|
|
1277
|
+
return null;
|
|
1278
|
+
}
|
|
1279
|
+
return piece.color === "w" ? piece.type.toUpperCase() : piece.type;
|
|
1280
|
+
})
|
|
1281
|
+
);
|
|
1282
|
+
}
|
|
1283
|
+
function parseFEN(fen) {
|
|
1284
|
+
const chess = new import_chess.Chess();
|
|
1285
|
+
try {
|
|
1286
|
+
chess.load(fen);
|
|
1287
|
+
} catch (error) {
|
|
1288
|
+
throw new ParseError("Invalid FEN", { cause: error });
|
|
1289
|
+
}
|
|
1290
|
+
return parseBoardArray(chessBoardToBoardArray(chess.board()));
|
|
1291
|
+
}
|
|
1292
|
+
function parsePGN(pgn) {
|
|
1293
|
+
const chess = new import_chess.Chess();
|
|
1294
|
+
try {
|
|
1295
|
+
chess.loadPgn(pgn);
|
|
1296
|
+
} catch (error) {
|
|
1297
|
+
throw new ParseError("Invalid PGN", { cause: error });
|
|
1298
|
+
}
|
|
1299
|
+
return parseBoardArray(chessBoardToBoardArray(chess.board()));
|
|
1300
|
+
}
|
|
1301
|
+
function parseBoardArray(board) {
|
|
1302
|
+
const validatedBoard = validateBoardArray(board);
|
|
1303
|
+
const position = createEmptyBoardPosition();
|
|
1304
|
+
validatedBoard.forEach((rank, rankIndex) => {
|
|
1305
|
+
rank.forEach((cell, fileIndex) => {
|
|
1306
|
+
if (cell === null) {
|
|
1307
|
+
return;
|
|
1308
|
+
}
|
|
1309
|
+
const pieceKey = PIECE_SYMBOL_TO_KEY[cell];
|
|
1310
|
+
if (!pieceKey) {
|
|
1311
|
+
throw new ValidationError(`Invalid board piece: ${cell}`);
|
|
1312
|
+
}
|
|
1313
|
+
const square = `${FILES[fileIndex]}${8 - rankIndex}`;
|
|
1314
|
+
position.squares[square] = pieceKey;
|
|
1315
|
+
});
|
|
1316
|
+
});
|
|
1317
|
+
return position;
|
|
1318
|
+
}
|
|
1319
|
+
|
|
1320
|
+
// src/api/render-request.ts
|
|
1321
|
+
function parseInputPosition(options) {
|
|
1322
|
+
if (typeof options.fen === "string") {
|
|
1323
|
+
return parseFEN(options.fen);
|
|
1324
|
+
}
|
|
1325
|
+
if (typeof options.pgn === "string") {
|
|
1326
|
+
return parsePGN(options.pgn);
|
|
1327
|
+
}
|
|
1328
|
+
if (Array.isArray(options.board)) {
|
|
1329
|
+
return parseBoardArray(options.board);
|
|
1330
|
+
}
|
|
1331
|
+
throw new ValidationError("Exactly one of fen, pgn, or board must be provided");
|
|
1332
|
+
}
|
|
1333
|
+
function validateSingleInputSource(options) {
|
|
1334
|
+
const provided = [
|
|
1335
|
+
typeof options.fen === "string" ? options.fen : void 0,
|
|
1336
|
+
typeof options.pgn === "string" ? options.pgn : void 0,
|
|
1337
|
+
Array.isArray(options.board) ? options.board : void 0
|
|
1338
|
+
].filter((value) => value !== void 0);
|
|
1339
|
+
if (provided.length !== 1) {
|
|
1340
|
+
throw new ValidationError("Exactly one of fen, pgn, or board must be provided");
|
|
1341
|
+
}
|
|
1342
|
+
}
|
|
1343
|
+
function createRenderRequest(board, options) {
|
|
1344
|
+
const normalized = normalizeRenderInputs(options);
|
|
1345
|
+
return {
|
|
1346
|
+
board,
|
|
1347
|
+
theme: normalized.theme,
|
|
1348
|
+
highlights: normalized.highlights,
|
|
1349
|
+
size: normalized.size,
|
|
1350
|
+
padding: normalized.padding,
|
|
1351
|
+
borderSize: normalized.borderSize,
|
|
1352
|
+
flipped: normalized.flipped,
|
|
1353
|
+
colors: normalized.colors,
|
|
1354
|
+
coordinates: normalized.coordinates
|
|
1355
|
+
};
|
|
1356
|
+
}
|
|
1357
|
+
function createRenderRequestFromOptions(options) {
|
|
1358
|
+
validateSingleInputSource(options);
|
|
1359
|
+
const board = parseInputPosition(options);
|
|
1360
|
+
return createRenderRequest(board, options);
|
|
1361
|
+
}
|
|
1362
|
+
|
|
1026
1363
|
// src/api/class-api.ts
|
|
1027
1364
|
var ChessImageGenerator = class {
|
|
1028
1365
|
position = null;
|
|
@@ -1056,65 +1393,70 @@ var ChessImageGenerator = class {
|
|
|
1056
1393
|
throw new ValidationError("No board position loaded");
|
|
1057
1394
|
}
|
|
1058
1395
|
const renderer = new CanvasPngRenderer();
|
|
1059
|
-
const
|
|
1396
|
+
const request = createRenderRequest(this.position, {
|
|
1060
1397
|
...this.defaults,
|
|
1061
1398
|
highlights: this.highlights,
|
|
1062
1399
|
highlightSquares: void 0
|
|
1063
1400
|
});
|
|
1064
|
-
return renderer.render(
|
|
1065
|
-
board: this.position,
|
|
1066
|
-
theme: normalized.theme,
|
|
1067
|
-
highlights: normalized.highlights,
|
|
1068
|
-
size: normalized.size,
|
|
1069
|
-
padding: normalized.padding,
|
|
1070
|
-
borderSize: normalized.borderSize,
|
|
1071
|
-
flipped: normalized.flipped,
|
|
1072
|
-
colors: normalized.colors,
|
|
1073
|
-
coordinates: normalized.coordinates
|
|
1074
|
-
});
|
|
1401
|
+
return renderer.render(request);
|
|
1075
1402
|
}
|
|
1076
1403
|
async toFile(filePath) {
|
|
1077
1404
|
const buffer = await this.toBuffer();
|
|
1078
1405
|
await writeBufferToFile(filePath, buffer);
|
|
1079
1406
|
}
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1407
|
+
async toSvg() {
|
|
1408
|
+
if (!this.position) {
|
|
1409
|
+
throw new ValidationError("No board position loaded");
|
|
1410
|
+
}
|
|
1411
|
+
const renderer = new SvgRenderer();
|
|
1412
|
+
const request = createRenderRequest(this.position, {
|
|
1413
|
+
...this.defaults,
|
|
1414
|
+
highlights: this.highlights,
|
|
1415
|
+
highlightSquares: void 0
|
|
1416
|
+
});
|
|
1417
|
+
return renderer.render(request);
|
|
1086
1418
|
}
|
|
1087
|
-
|
|
1088
|
-
|
|
1419
|
+
async toSvgFile(filePath) {
|
|
1420
|
+
await writeStringToFile(filePath, await this.toSvg());
|
|
1089
1421
|
}
|
|
1090
|
-
|
|
1091
|
-
|
|
1422
|
+
async toJpeg() {
|
|
1423
|
+
if (!this.position) {
|
|
1424
|
+
throw new ValidationError("No board position loaded");
|
|
1425
|
+
}
|
|
1426
|
+
const renderer = new CanvasJpegRenderer();
|
|
1427
|
+
const request = createRenderRequest(this.position, {
|
|
1428
|
+
...this.defaults,
|
|
1429
|
+
highlights: this.highlights,
|
|
1430
|
+
highlightSquares: void 0
|
|
1431
|
+
});
|
|
1432
|
+
return renderer.render(request);
|
|
1092
1433
|
}
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
async function renderChess(options) {
|
|
1096
|
-
const provided = [
|
|
1097
|
-
typeof options.fen === "string" ? options.fen : void 0,
|
|
1098
|
-
typeof options.pgn === "string" ? options.pgn : void 0,
|
|
1099
|
-
Array.isArray(options.board) ? options.board : void 0
|
|
1100
|
-
].filter((value) => value !== void 0);
|
|
1101
|
-
if (provided.length !== 1) {
|
|
1102
|
-
throw new ValidationError("Exactly one of fen, pgn, or board must be provided");
|
|
1434
|
+
async toJpegFile(filePath) {
|
|
1435
|
+
await writeBufferToFile(filePath, await this.toJpeg());
|
|
1103
1436
|
}
|
|
1104
|
-
|
|
1437
|
+
};
|
|
1438
|
+
|
|
1439
|
+
// src/api/functional-api.ts
|
|
1440
|
+
async function renderChess(options) {
|
|
1105
1441
|
const renderer = new CanvasPngRenderer();
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
1442
|
+
return renderer.render(createRenderRequestFromOptions(options));
|
|
1443
|
+
}
|
|
1444
|
+
async function renderSvg(options) {
|
|
1445
|
+
const renderer = new SvgRenderer();
|
|
1446
|
+
return renderer.render(createRenderRequestFromOptions(options));
|
|
1447
|
+
}
|
|
1448
|
+
async function renderJpeg(options) {
|
|
1449
|
+
const renderer = new CanvasJpegRenderer();
|
|
1450
|
+
return renderer.render(createRenderRequestFromOptions(options));
|
|
1451
|
+
}
|
|
1452
|
+
async function renderFile(filePath, options) {
|
|
1453
|
+
await writeBufferToFile(filePath, await renderChess(options));
|
|
1454
|
+
}
|
|
1455
|
+
async function renderSvgFile(filePath, options) {
|
|
1456
|
+
await writeStringToFile(filePath, await renderSvg(options));
|
|
1457
|
+
}
|
|
1458
|
+
async function renderJpegFile(filePath, options) {
|
|
1459
|
+
await writeBufferToFile(filePath, await renderJpeg(options));
|
|
1118
1460
|
}
|
|
1119
1461
|
// Annotate the CommonJS export names for ESM import in node:
|
|
1120
1462
|
0 && (module.exports = {
|
|
@@ -1125,6 +1467,11 @@ async function renderChess(options) {
|
|
|
1125
1467
|
ThemeError,
|
|
1126
1468
|
ValidationError,
|
|
1127
1469
|
registerTheme,
|
|
1128
|
-
renderChess
|
|
1470
|
+
renderChess,
|
|
1471
|
+
renderFile,
|
|
1472
|
+
renderJpeg,
|
|
1473
|
+
renderJpegFile,
|
|
1474
|
+
renderSvg,
|
|
1475
|
+
renderSvgFile
|
|
1129
1476
|
});
|
|
1130
1477
|
//# sourceMappingURL=index.cjs.map
|