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.js
CHANGED
|
@@ -37,23 +37,6 @@ var IOError = class extends Error {
|
|
|
37
37
|
}
|
|
38
38
|
};
|
|
39
39
|
|
|
40
|
-
// src/core/parsers.ts
|
|
41
|
-
import { Chess } from "chess.js";
|
|
42
|
-
|
|
43
|
-
// src/core/board.ts
|
|
44
|
-
var FILES = ["a", "b", "c", "d", "e", "f", "g", "h"];
|
|
45
|
-
var RANKS = ["8", "7", "6", "5", "4", "3", "2", "1"];
|
|
46
|
-
var SQUARES = RANKS.flatMap(
|
|
47
|
-
(rank) => FILES.map((file) => `${file}${rank}`)
|
|
48
|
-
);
|
|
49
|
-
function createEmptyBoardPosition() {
|
|
50
|
-
return {
|
|
51
|
-
squares: Object.fromEntries(
|
|
52
|
-
SQUARES.map((square) => [square, null])
|
|
53
|
-
)
|
|
54
|
-
};
|
|
55
|
-
}
|
|
56
|
-
|
|
57
40
|
// src/core/validators.ts
|
|
58
41
|
import { createCanvas } from "canvas";
|
|
59
42
|
var SQUARE_PATTERN = /^[a-h][1-8]$/;
|
|
@@ -223,6 +206,9 @@ function validateHighlightEntry(entry) {
|
|
|
223
206
|
if (entry.lineWidth !== void 0 && (!Number.isFinite(entry.lineWidth) || entry.lineWidth <= 0)) {
|
|
224
207
|
throw new ValidationError("highlight.lineWidth must be a finite number greater than 0");
|
|
225
208
|
}
|
|
209
|
+
if (entry.radius !== void 0 && (!Number.isFinite(entry.radius) || entry.radius <= 0 || entry.radius > 0.5)) {
|
|
210
|
+
throw new ValidationError("highlight.radius must be a finite number greater than 0 and at most 0.5");
|
|
211
|
+
}
|
|
226
212
|
}
|
|
227
213
|
function validateHighlightOptions(highlights) {
|
|
228
214
|
if (highlights === void 0) {
|
|
@@ -243,71 +229,23 @@ function validateHighlightsInput(highlights, highlightSquares) {
|
|
|
243
229
|
validateHighlightOptions(highlightSquares);
|
|
244
230
|
}
|
|
245
231
|
|
|
246
|
-
// src/core/parsers.ts
|
|
247
|
-
var PIECE_SYMBOL_TO_KEY = {
|
|
248
|
-
K: "wK",
|
|
249
|
-
Q: "wQ",
|
|
250
|
-
R: "wR",
|
|
251
|
-
B: "wB",
|
|
252
|
-
N: "wN",
|
|
253
|
-
P: "wP",
|
|
254
|
-
k: "bK",
|
|
255
|
-
q: "bQ",
|
|
256
|
-
r: "bR",
|
|
257
|
-
b: "bB",
|
|
258
|
-
n: "bN",
|
|
259
|
-
p: "bP"
|
|
260
|
-
};
|
|
261
|
-
function chessBoardToBoardArray(board) {
|
|
262
|
-
return board.map(
|
|
263
|
-
(rank) => rank.map((piece) => {
|
|
264
|
-
if (!piece) {
|
|
265
|
-
return null;
|
|
266
|
-
}
|
|
267
|
-
return piece.color === "w" ? piece.type.toUpperCase() : piece.type;
|
|
268
|
-
})
|
|
269
|
-
);
|
|
270
|
-
}
|
|
271
|
-
function parseFEN(fen) {
|
|
272
|
-
const chess = new Chess();
|
|
273
|
-
try {
|
|
274
|
-
chess.load(fen);
|
|
275
|
-
} catch (error) {
|
|
276
|
-
throw new ParseError("Invalid FEN", { cause: error });
|
|
277
|
-
}
|
|
278
|
-
return parseBoardArray(chessBoardToBoardArray(chess.board()));
|
|
279
|
-
}
|
|
280
|
-
function parsePGN(pgn) {
|
|
281
|
-
const chess = new Chess();
|
|
282
|
-
try {
|
|
283
|
-
chess.loadPgn(pgn);
|
|
284
|
-
} catch (error) {
|
|
285
|
-
throw new ParseError("Invalid PGN", { cause: error });
|
|
286
|
-
}
|
|
287
|
-
return parseBoardArray(chessBoardToBoardArray(chess.board()));
|
|
288
|
-
}
|
|
289
|
-
function parseBoardArray(board) {
|
|
290
|
-
const validatedBoard = validateBoardArray(board);
|
|
291
|
-
const position = createEmptyBoardPosition();
|
|
292
|
-
validatedBoard.forEach((rank, rankIndex) => {
|
|
293
|
-
rank.forEach((cell, fileIndex) => {
|
|
294
|
-
if (cell === null) {
|
|
295
|
-
return;
|
|
296
|
-
}
|
|
297
|
-
const pieceKey = PIECE_SYMBOL_TO_KEY[cell];
|
|
298
|
-
if (!pieceKey) {
|
|
299
|
-
throw new ValidationError(`Invalid board piece: ${cell}`);
|
|
300
|
-
}
|
|
301
|
-
const square = `${FILES[fileIndex]}${8 - rankIndex}`;
|
|
302
|
-
position.squares[square] = pieceKey;
|
|
303
|
-
});
|
|
304
|
-
});
|
|
305
|
-
return position;
|
|
306
|
-
}
|
|
307
|
-
|
|
308
232
|
// src/render/canvas-renderer.ts
|
|
309
233
|
import { createCanvas as createCanvas3 } from "canvas";
|
|
310
234
|
|
|
235
|
+
// src/core/board.ts
|
|
236
|
+
var FILES = ["a", "b", "c", "d", "e", "f", "g", "h"];
|
|
237
|
+
var RANKS = ["8", "7", "6", "5", "4", "3", "2", "1"];
|
|
238
|
+
var SQUARES = RANKS.flatMap(
|
|
239
|
+
(rank) => FILES.map((file) => `${file}${rank}`)
|
|
240
|
+
);
|
|
241
|
+
function createEmptyBoardPosition() {
|
|
242
|
+
return {
|
|
243
|
+
squares: Object.fromEntries(
|
|
244
|
+
SQUARES.map((square) => [square, null])
|
|
245
|
+
)
|
|
246
|
+
};
|
|
247
|
+
}
|
|
248
|
+
|
|
311
249
|
// src/core/geometry.ts
|
|
312
250
|
function createBoardGeometry({
|
|
313
251
|
size,
|
|
@@ -543,6 +481,10 @@ function resolveCircleLineWidth(squareSize, lineWidth) {
|
|
|
543
481
|
const candidate = lineWidth ?? squareSize * 0.08;
|
|
544
482
|
return Math.max(2, Math.min(8, candidate));
|
|
545
483
|
}
|
|
484
|
+
function resolveCircleRadius(squareSize, radius, lineWidth) {
|
|
485
|
+
const radiusPx = squareSize * (radius ?? 0.42);
|
|
486
|
+
return Math.max(0, radiusPx - lineWidth / 2);
|
|
487
|
+
}
|
|
546
488
|
function resolveBorderCoordinateFontSize(context, geometry) {
|
|
547
489
|
const maxFontSize = Math.floor(
|
|
548
490
|
Math.min(geometry.squareSize * 0.6, geometry.borderSize * 0.65)
|
|
@@ -692,11 +634,16 @@ function drawCircleHighlights(context, request, geometry) {
|
|
|
692
634
|
squareGeometry.size,
|
|
693
635
|
highlight.lineWidth
|
|
694
636
|
);
|
|
637
|
+
const radius = resolveCircleRadius(
|
|
638
|
+
squareGeometry.size,
|
|
639
|
+
highlight.radius,
|
|
640
|
+
context.lineWidth
|
|
641
|
+
);
|
|
695
642
|
context.beginPath();
|
|
696
643
|
context.arc(
|
|
697
644
|
centerX,
|
|
698
645
|
centerY,
|
|
699
|
-
|
|
646
|
+
radius,
|
|
700
647
|
0,
|
|
701
648
|
Math.PI * 2
|
|
702
649
|
);
|
|
@@ -727,6 +674,9 @@ async function drawPieces(context, request, geometry) {
|
|
|
727
674
|
}
|
|
728
675
|
}
|
|
729
676
|
var CanvasPngRenderer = class {
|
|
677
|
+
createOutputBuffer(canvas) {
|
|
678
|
+
return canvas.toBuffer("image/png");
|
|
679
|
+
}
|
|
730
680
|
async render(request) {
|
|
731
681
|
try {
|
|
732
682
|
const geometry = createBoardGeometry({
|
|
@@ -744,7 +694,7 @@ var CanvasPngRenderer = class {
|
|
|
744
694
|
drawCircleHighlights(context, request, geometry);
|
|
745
695
|
drawCoordinates(context, request, geometry);
|
|
746
696
|
await drawPieces(context, request, geometry);
|
|
747
|
-
return
|
|
697
|
+
return this.createOutputBuffer(canvas);
|
|
748
698
|
} catch (error) {
|
|
749
699
|
if (error instanceof RenderError) {
|
|
750
700
|
throw error;
|
|
@@ -754,6 +704,270 @@ var CanvasPngRenderer = class {
|
|
|
754
704
|
}
|
|
755
705
|
};
|
|
756
706
|
|
|
707
|
+
// src/render/canvas-jpeg-renderer.ts
|
|
708
|
+
import "canvas";
|
|
709
|
+
var CanvasJpegRenderer = class extends CanvasPngRenderer {
|
|
710
|
+
createOutputBuffer(canvas) {
|
|
711
|
+
return canvas.toBuffer("image/jpeg");
|
|
712
|
+
}
|
|
713
|
+
};
|
|
714
|
+
|
|
715
|
+
// src/render/svg-renderer.ts
|
|
716
|
+
import { readFile as readFile2 } from "fs/promises";
|
|
717
|
+
import { createCanvas as createCanvas5 } from "canvas";
|
|
718
|
+
var svgSourceCache2 = new SourceAssetCache();
|
|
719
|
+
var binarySourceCache = new SourceAssetCache();
|
|
720
|
+
var measureContext = createCanvas5(1, 1).getContext("2d");
|
|
721
|
+
var MIN_COORDINATE_FONT_SIZE2 = 8;
|
|
722
|
+
var MAX_FILE_LABEL_WIDTH_RATIO2 = 0.75;
|
|
723
|
+
var MAX_RANK_LABEL_WIDTH_RATIO2 = 0.7;
|
|
724
|
+
var MAX_LABEL_HEIGHT_RATIO2 = 0.7;
|
|
725
|
+
var INSIDE_COORDINATE_MAX_FONT_RATIO2 = 0.34;
|
|
726
|
+
var INSIDE_LIGHT_LABEL_COLOR2 = "rgba(255,255,255,0.6)";
|
|
727
|
+
var INSIDE_DARK_LABEL_COLOR2 = "rgba(0,0,0,0.45)";
|
|
728
|
+
function escapeXml(value) {
|
|
729
|
+
return value.replaceAll("&", "&").replaceAll("<", "<").replaceAll(">", ">").replaceAll('"', """).replaceAll("'", "'");
|
|
730
|
+
}
|
|
731
|
+
function isDarkSquare2(square) {
|
|
732
|
+
const fileIndex = square.charCodeAt(0) - 97;
|
|
733
|
+
const rankNumber = Number(square[1]);
|
|
734
|
+
return (fileIndex + rankNumber) % 2 === 1;
|
|
735
|
+
}
|
|
736
|
+
function resolveInsideLabelColor2(request, square) {
|
|
737
|
+
if (request.coordinates.color) {
|
|
738
|
+
return request.coordinates.color;
|
|
739
|
+
}
|
|
740
|
+
return isDarkSquare2(square) ? INSIDE_LIGHT_LABEL_COLOR2 : INSIDE_DARK_LABEL_COLOR2;
|
|
741
|
+
}
|
|
742
|
+
function resolveHighlightOpacity2(style, color, opacity) {
|
|
743
|
+
if (opacity !== void 0) {
|
|
744
|
+
return opacity;
|
|
745
|
+
}
|
|
746
|
+
if (style === "circle" || color !== void 0) {
|
|
747
|
+
return 0.9;
|
|
748
|
+
}
|
|
749
|
+
return 1;
|
|
750
|
+
}
|
|
751
|
+
function resolveCircleLineWidth2(squareSize, lineWidth) {
|
|
752
|
+
const candidate = lineWidth ?? squareSize * 0.08;
|
|
753
|
+
return Math.max(2, Math.min(8, candidate));
|
|
754
|
+
}
|
|
755
|
+
function resolveCircleRadius2(squareSize, radius, lineWidth) {
|
|
756
|
+
const radiusPx = squareSize * (radius ?? 0.42);
|
|
757
|
+
return Math.max(0, radiusPx - lineWidth / 2);
|
|
758
|
+
}
|
|
759
|
+
function resolveBorderCoordinateFontSize2(geometry) {
|
|
760
|
+
const maxFontSize = Math.floor(
|
|
761
|
+
Math.min(geometry.squareSize * 0.6, geometry.borderSize * 0.65)
|
|
762
|
+
);
|
|
763
|
+
for (let candidate = maxFontSize; candidate >= MIN_COORDINATE_FONT_SIZE2; candidate -= 1) {
|
|
764
|
+
measureContext.font = `${candidate}px sans-serif`;
|
|
765
|
+
const filesFit = geometry.borderFileLabels.every((label) => {
|
|
766
|
+
const metrics = measureContext.measureText(label.text);
|
|
767
|
+
const textHeight = metrics.actualBoundingBoxAscent + metrics.actualBoundingBoxDescent;
|
|
768
|
+
return metrics.width <= geometry.squareSize * MAX_FILE_LABEL_WIDTH_RATIO2 && textHeight <= geometry.borderSize * MAX_LABEL_HEIGHT_RATIO2;
|
|
769
|
+
});
|
|
770
|
+
const ranksFit = geometry.borderRankLabels.every((label) => {
|
|
771
|
+
const metrics = measureContext.measureText(label.text);
|
|
772
|
+
const textHeight = metrics.actualBoundingBoxAscent + metrics.actualBoundingBoxDescent;
|
|
773
|
+
return metrics.width <= geometry.borderSize * MAX_RANK_LABEL_WIDTH_RATIO2 && textHeight <= geometry.squareSize * MAX_LABEL_HEIGHT_RATIO2;
|
|
774
|
+
});
|
|
775
|
+
if (filesFit && ranksFit) {
|
|
776
|
+
return candidate;
|
|
777
|
+
}
|
|
778
|
+
}
|
|
779
|
+
return null;
|
|
780
|
+
}
|
|
781
|
+
function resolveInsideCoordinateFontSize2(geometry) {
|
|
782
|
+
const maxFontSize = Math.floor(
|
|
783
|
+
geometry.squareSize * INSIDE_COORDINATE_MAX_FONT_RATIO2
|
|
784
|
+
);
|
|
785
|
+
for (let candidate = maxFontSize; candidate >= MIN_COORDINATE_FONT_SIZE2; candidate -= 1) {
|
|
786
|
+
measureContext.font = `${candidate}px sans-serif`;
|
|
787
|
+
const filesFit = geometry.insideFileLabels.every((label) => {
|
|
788
|
+
const metrics = measureContext.measureText(label.text);
|
|
789
|
+
const textHeight = metrics.actualBoundingBoxAscent + metrics.actualBoundingBoxDescent;
|
|
790
|
+
return metrics.width <= geometry.insideLabelMaxWidth && textHeight <= geometry.insideLabelMaxHeight;
|
|
791
|
+
});
|
|
792
|
+
const ranksFit = geometry.insideRankLabels.every((label) => {
|
|
793
|
+
const metrics = measureContext.measureText(label.text);
|
|
794
|
+
const textHeight = metrics.actualBoundingBoxAscent + metrics.actualBoundingBoxDescent;
|
|
795
|
+
return metrics.width <= geometry.insideLabelMaxWidth && textHeight <= geometry.insideLabelMaxHeight;
|
|
796
|
+
});
|
|
797
|
+
if (filesFit && ranksFit) {
|
|
798
|
+
return candidate;
|
|
799
|
+
}
|
|
800
|
+
}
|
|
801
|
+
return null;
|
|
802
|
+
}
|
|
803
|
+
function textAnchor(value) {
|
|
804
|
+
if (value === "center") {
|
|
805
|
+
return "middle";
|
|
806
|
+
}
|
|
807
|
+
return value === "right" ? "end" : "start";
|
|
808
|
+
}
|
|
809
|
+
function dominantBaseline(value) {
|
|
810
|
+
if (value === "middle") {
|
|
811
|
+
return "middle";
|
|
812
|
+
}
|
|
813
|
+
return value === "bottom" ? "text-after-edge" : "hanging";
|
|
814
|
+
}
|
|
815
|
+
async function readSvgSource2(filePath) {
|
|
816
|
+
const cached = svgSourceCache2.get(filePath);
|
|
817
|
+
if (cached) {
|
|
818
|
+
return cached;
|
|
819
|
+
}
|
|
820
|
+
try {
|
|
821
|
+
const source = await readFile2(filePath, "utf8");
|
|
822
|
+
svgSourceCache2.set(filePath, source);
|
|
823
|
+
return source;
|
|
824
|
+
} catch (error) {
|
|
825
|
+
throw new RenderError(`Failed to read SVG asset: ${filePath}`, { cause: error });
|
|
826
|
+
}
|
|
827
|
+
}
|
|
828
|
+
async function readBinarySource(filePath) {
|
|
829
|
+
const cached = binarySourceCache.get(filePath);
|
|
830
|
+
if (cached) {
|
|
831
|
+
return cached;
|
|
832
|
+
}
|
|
833
|
+
try {
|
|
834
|
+
const source = await readFile2(filePath);
|
|
835
|
+
binarySourceCache.set(filePath, source);
|
|
836
|
+
return source;
|
|
837
|
+
} catch (error) {
|
|
838
|
+
throw new RenderError(`Failed to read image asset: ${filePath}`, { cause: error });
|
|
839
|
+
}
|
|
840
|
+
}
|
|
841
|
+
function stripSvgPreamble(source) {
|
|
842
|
+
return source.replace(/^\uFEFF/, "").replace(/<\?xml[\s\S]*?\?>/gi, "").replace(/<!doctype[\s\S]*?>/gi, "").trim();
|
|
843
|
+
}
|
|
844
|
+
function inlineSvgPiece(source, x, y, size) {
|
|
845
|
+
const sanitized = stripSvgPreamble(source);
|
|
846
|
+
if (!sanitized.startsWith("<svg")) {
|
|
847
|
+
throw new RenderError("Invalid SVG asset source");
|
|
848
|
+
}
|
|
849
|
+
return sanitized.replace(/<svg\b([^>]*)>/i, (_match, attrs) => {
|
|
850
|
+
const cleanedAttrs = attrs.replace(/\s(?:x|y|width|height)=(".*?"|'.*?'|[^\s>]+)/gi, "").trim();
|
|
851
|
+
const preservedAttrs = cleanedAttrs ? ` ${cleanedAttrs}` : "";
|
|
852
|
+
return `<svg x="${x}" y="${y}" width="${size}" height="${size}"${preservedAttrs}>`;
|
|
853
|
+
});
|
|
854
|
+
}
|
|
855
|
+
async function renderPieceElement(asset, x, y, size) {
|
|
856
|
+
if (asset.kind === "svg") {
|
|
857
|
+
const source = await readSvgSource2(asset.source);
|
|
858
|
+
return inlineSvgPiece(source, x, y, size);
|
|
859
|
+
}
|
|
860
|
+
const buffer = await readBinarySource(asset.source);
|
|
861
|
+
return [
|
|
862
|
+
`<image x="${x}" y="${y}" width="${size}" height="${size}"`,
|
|
863
|
+
` href="data:image/png;base64,${buffer.toString("base64")}" />`
|
|
864
|
+
].join("");
|
|
865
|
+
}
|
|
866
|
+
async function renderPieces(request, geometry) {
|
|
867
|
+
const pieces = [];
|
|
868
|
+
for (const square of SQUARES) {
|
|
869
|
+
const pieceKey = request.board.squares[square];
|
|
870
|
+
if (!pieceKey) {
|
|
871
|
+
continue;
|
|
872
|
+
}
|
|
873
|
+
const squareGeometry = geometry.squares[square];
|
|
874
|
+
pieces.push(
|
|
875
|
+
await renderPieceElement(
|
|
876
|
+
request.theme.pieces[pieceKey],
|
|
877
|
+
squareGeometry.x,
|
|
878
|
+
squareGeometry.y,
|
|
879
|
+
squareGeometry.size
|
|
880
|
+
)
|
|
881
|
+
);
|
|
882
|
+
}
|
|
883
|
+
return pieces;
|
|
884
|
+
}
|
|
885
|
+
function renderCoordinates(request, geometry) {
|
|
886
|
+
if (!request.coordinates.enabled) {
|
|
887
|
+
return [];
|
|
888
|
+
}
|
|
889
|
+
if (request.coordinates.position === "border") {
|
|
890
|
+
if (geometry.borderSize === 0) {
|
|
891
|
+
return [];
|
|
892
|
+
}
|
|
893
|
+
const fontSize2 = resolveBorderCoordinateFontSize2(geometry);
|
|
894
|
+
if (fontSize2 === null) {
|
|
895
|
+
return [];
|
|
896
|
+
}
|
|
897
|
+
return [
|
|
898
|
+
...geometry.borderFileLabels,
|
|
899
|
+
...geometry.borderRankLabels
|
|
900
|
+
].map(
|
|
901
|
+
(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>`
|
|
902
|
+
);
|
|
903
|
+
}
|
|
904
|
+
const fontSize = resolveInsideCoordinateFontSize2(geometry);
|
|
905
|
+
if (fontSize === null) {
|
|
906
|
+
return [];
|
|
907
|
+
}
|
|
908
|
+
return [
|
|
909
|
+
...geometry.insideFileLabels.map(
|
|
910
|
+
(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>`
|
|
911
|
+
),
|
|
912
|
+
...geometry.insideRankLabels.map(
|
|
913
|
+
(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>`
|
|
914
|
+
)
|
|
915
|
+
];
|
|
916
|
+
}
|
|
917
|
+
var SvgRenderer = class {
|
|
918
|
+
async render(request) {
|
|
919
|
+
try {
|
|
920
|
+
const geometry = createBoardGeometry({
|
|
921
|
+
size: request.size,
|
|
922
|
+
padding: request.padding,
|
|
923
|
+
borderSize: request.borderSize,
|
|
924
|
+
flipped: request.flipped
|
|
925
|
+
});
|
|
926
|
+
const elements = [
|
|
927
|
+
`<rect x="0" y="0" width="${geometry.imageWidth}" height="${geometry.imageHeight}" fill="${escapeXml(request.colors.lightSquare)}" />`
|
|
928
|
+
];
|
|
929
|
+
for (const square of SQUARES) {
|
|
930
|
+
const squareGeometry = geometry.squares[square];
|
|
931
|
+
elements.push(
|
|
932
|
+
`<rect x="${squareGeometry.x}" y="${squareGeometry.y}" width="${squareGeometry.size}" height="${squareGeometry.size}" fill="${escapeXml(isDarkSquare2(square) ? request.colors.darkSquare : request.colors.lightSquare)}" />`
|
|
933
|
+
);
|
|
934
|
+
}
|
|
935
|
+
for (const highlight of request.highlights) {
|
|
936
|
+
if (highlight.style !== "fill") {
|
|
937
|
+
continue;
|
|
938
|
+
}
|
|
939
|
+
const squareGeometry = geometry.squares[highlight.square];
|
|
940
|
+
elements.push(
|
|
941
|
+
`<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)}" />`
|
|
942
|
+
);
|
|
943
|
+
}
|
|
944
|
+
for (const highlight of request.highlights) {
|
|
945
|
+
if (highlight.style !== "circle") {
|
|
946
|
+
continue;
|
|
947
|
+
}
|
|
948
|
+
const squareGeometry = geometry.squares[highlight.square];
|
|
949
|
+
const lineWidth = resolveCircleLineWidth2(squareGeometry.size, highlight.lineWidth);
|
|
950
|
+
const radius = resolveCircleRadius2(squareGeometry.size, highlight.radius, lineWidth);
|
|
951
|
+
elements.push(
|
|
952
|
+
`<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)}" />`
|
|
953
|
+
);
|
|
954
|
+
}
|
|
955
|
+
elements.push(...renderCoordinates(request, geometry));
|
|
956
|
+
elements.push(...await renderPieces(request, geometry));
|
|
957
|
+
return [
|
|
958
|
+
`<svg xmlns="http://www.w3.org/2000/svg" width="${geometry.imageWidth}" height="${geometry.imageHeight}" viewBox="0 0 ${geometry.imageWidth} ${geometry.imageHeight}">`,
|
|
959
|
+
...elements,
|
|
960
|
+
"</svg>"
|
|
961
|
+
].join("");
|
|
962
|
+
} catch (error) {
|
|
963
|
+
if (error instanceof RenderError) {
|
|
964
|
+
throw error;
|
|
965
|
+
}
|
|
966
|
+
throw new RenderError("Failed to render chess board as SVG", { cause: error });
|
|
967
|
+
}
|
|
968
|
+
}
|
|
969
|
+
};
|
|
970
|
+
|
|
757
971
|
// src/utils/io.ts
|
|
758
972
|
import { writeFile } from "fs/promises";
|
|
759
973
|
async function writeBufferToFile(filePath, buffer) {
|
|
@@ -763,6 +977,13 @@ async function writeBufferToFile(filePath, buffer) {
|
|
|
763
977
|
throw new IOError(`Failed to write file: ${filePath}`, { cause: error });
|
|
764
978
|
}
|
|
765
979
|
}
|
|
980
|
+
async function writeStringToFile(filePath, contents) {
|
|
981
|
+
try {
|
|
982
|
+
await writeFile(filePath, contents, "utf8");
|
|
983
|
+
} catch (error) {
|
|
984
|
+
throw new IOError(`Failed to write file: ${filePath}`, { cause: error });
|
|
985
|
+
}
|
|
986
|
+
}
|
|
766
987
|
|
|
767
988
|
// src/core/highlights.ts
|
|
768
989
|
function normalizeHighlightEntries(input) {
|
|
@@ -770,7 +991,11 @@ function normalizeHighlightEntries(input) {
|
|
|
770
991
|
if (typeof entry === "string") {
|
|
771
992
|
return {
|
|
772
993
|
square: validateSquare(entry),
|
|
773
|
-
style: "fill"
|
|
994
|
+
style: "fill",
|
|
995
|
+
color: void 0,
|
|
996
|
+
opacity: void 0,
|
|
997
|
+
lineWidth: void 0,
|
|
998
|
+
radius: void 0
|
|
774
999
|
};
|
|
775
1000
|
}
|
|
776
1001
|
const style = entry.style ?? "fill";
|
|
@@ -779,7 +1004,8 @@ function normalizeHighlightEntries(input) {
|
|
|
779
1004
|
style,
|
|
780
1005
|
color: entry.color ?? (style === "circle" ? "#ffcc00" : void 0),
|
|
781
1006
|
opacity: entry.opacity ?? (style === "circle" ? 0.9 : void 0),
|
|
782
|
-
lineWidth: entry.lineWidth
|
|
1007
|
+
lineWidth: entry.lineWidth,
|
|
1008
|
+
radius: style === "circle" ? entry.radius ?? 0.42 : void 0
|
|
783
1009
|
};
|
|
784
1010
|
});
|
|
785
1011
|
}
|
|
@@ -997,6 +1223,112 @@ function normalizeRenderInputs(options) {
|
|
|
997
1223
|
};
|
|
998
1224
|
}
|
|
999
1225
|
|
|
1226
|
+
// src/core/parsers.ts
|
|
1227
|
+
import { Chess } from "chess.js";
|
|
1228
|
+
var PIECE_SYMBOL_TO_KEY = {
|
|
1229
|
+
K: "wK",
|
|
1230
|
+
Q: "wQ",
|
|
1231
|
+
R: "wR",
|
|
1232
|
+
B: "wB",
|
|
1233
|
+
N: "wN",
|
|
1234
|
+
P: "wP",
|
|
1235
|
+
k: "bK",
|
|
1236
|
+
q: "bQ",
|
|
1237
|
+
r: "bR",
|
|
1238
|
+
b: "bB",
|
|
1239
|
+
n: "bN",
|
|
1240
|
+
p: "bP"
|
|
1241
|
+
};
|
|
1242
|
+
function chessBoardToBoardArray(board) {
|
|
1243
|
+
return board.map(
|
|
1244
|
+
(rank) => rank.map((piece) => {
|
|
1245
|
+
if (!piece) {
|
|
1246
|
+
return null;
|
|
1247
|
+
}
|
|
1248
|
+
return piece.color === "w" ? piece.type.toUpperCase() : piece.type;
|
|
1249
|
+
})
|
|
1250
|
+
);
|
|
1251
|
+
}
|
|
1252
|
+
function parseFEN(fen) {
|
|
1253
|
+
const chess = new Chess();
|
|
1254
|
+
try {
|
|
1255
|
+
chess.load(fen);
|
|
1256
|
+
} catch (error) {
|
|
1257
|
+
throw new ParseError("Invalid FEN", { cause: error });
|
|
1258
|
+
}
|
|
1259
|
+
return parseBoardArray(chessBoardToBoardArray(chess.board()));
|
|
1260
|
+
}
|
|
1261
|
+
function parsePGN(pgn) {
|
|
1262
|
+
const chess = new Chess();
|
|
1263
|
+
try {
|
|
1264
|
+
chess.loadPgn(pgn);
|
|
1265
|
+
} catch (error) {
|
|
1266
|
+
throw new ParseError("Invalid PGN", { cause: error });
|
|
1267
|
+
}
|
|
1268
|
+
return parseBoardArray(chessBoardToBoardArray(chess.board()));
|
|
1269
|
+
}
|
|
1270
|
+
function parseBoardArray(board) {
|
|
1271
|
+
const validatedBoard = validateBoardArray(board);
|
|
1272
|
+
const position = createEmptyBoardPosition();
|
|
1273
|
+
validatedBoard.forEach((rank, rankIndex) => {
|
|
1274
|
+
rank.forEach((cell, fileIndex) => {
|
|
1275
|
+
if (cell === null) {
|
|
1276
|
+
return;
|
|
1277
|
+
}
|
|
1278
|
+
const pieceKey = PIECE_SYMBOL_TO_KEY[cell];
|
|
1279
|
+
if (!pieceKey) {
|
|
1280
|
+
throw new ValidationError(`Invalid board piece: ${cell}`);
|
|
1281
|
+
}
|
|
1282
|
+
const square = `${FILES[fileIndex]}${8 - rankIndex}`;
|
|
1283
|
+
position.squares[square] = pieceKey;
|
|
1284
|
+
});
|
|
1285
|
+
});
|
|
1286
|
+
return position;
|
|
1287
|
+
}
|
|
1288
|
+
|
|
1289
|
+
// src/api/render-request.ts
|
|
1290
|
+
function parseInputPosition(options) {
|
|
1291
|
+
if (typeof options.fen === "string") {
|
|
1292
|
+
return parseFEN(options.fen);
|
|
1293
|
+
}
|
|
1294
|
+
if (typeof options.pgn === "string") {
|
|
1295
|
+
return parsePGN(options.pgn);
|
|
1296
|
+
}
|
|
1297
|
+
if (Array.isArray(options.board)) {
|
|
1298
|
+
return parseBoardArray(options.board);
|
|
1299
|
+
}
|
|
1300
|
+
throw new ValidationError("Exactly one of fen, pgn, or board must be provided");
|
|
1301
|
+
}
|
|
1302
|
+
function validateSingleInputSource(options) {
|
|
1303
|
+
const provided = [
|
|
1304
|
+
typeof options.fen === "string" ? options.fen : void 0,
|
|
1305
|
+
typeof options.pgn === "string" ? options.pgn : void 0,
|
|
1306
|
+
Array.isArray(options.board) ? options.board : void 0
|
|
1307
|
+
].filter((value) => value !== void 0);
|
|
1308
|
+
if (provided.length !== 1) {
|
|
1309
|
+
throw new ValidationError("Exactly one of fen, pgn, or board must be provided");
|
|
1310
|
+
}
|
|
1311
|
+
}
|
|
1312
|
+
function createRenderRequest(board, options) {
|
|
1313
|
+
const normalized = normalizeRenderInputs(options);
|
|
1314
|
+
return {
|
|
1315
|
+
board,
|
|
1316
|
+
theme: normalized.theme,
|
|
1317
|
+
highlights: normalized.highlights,
|
|
1318
|
+
size: normalized.size,
|
|
1319
|
+
padding: normalized.padding,
|
|
1320
|
+
borderSize: normalized.borderSize,
|
|
1321
|
+
flipped: normalized.flipped,
|
|
1322
|
+
colors: normalized.colors,
|
|
1323
|
+
coordinates: normalized.coordinates
|
|
1324
|
+
};
|
|
1325
|
+
}
|
|
1326
|
+
function createRenderRequestFromOptions(options) {
|
|
1327
|
+
validateSingleInputSource(options);
|
|
1328
|
+
const board = parseInputPosition(options);
|
|
1329
|
+
return createRenderRequest(board, options);
|
|
1330
|
+
}
|
|
1331
|
+
|
|
1000
1332
|
// src/api/class-api.ts
|
|
1001
1333
|
var ChessImageGenerator = class {
|
|
1002
1334
|
position = null;
|
|
@@ -1030,65 +1362,70 @@ var ChessImageGenerator = class {
|
|
|
1030
1362
|
throw new ValidationError("No board position loaded");
|
|
1031
1363
|
}
|
|
1032
1364
|
const renderer = new CanvasPngRenderer();
|
|
1033
|
-
const
|
|
1365
|
+
const request = createRenderRequest(this.position, {
|
|
1034
1366
|
...this.defaults,
|
|
1035
1367
|
highlights: this.highlights,
|
|
1036
1368
|
highlightSquares: void 0
|
|
1037
1369
|
});
|
|
1038
|
-
return renderer.render(
|
|
1039
|
-
board: this.position,
|
|
1040
|
-
theme: normalized.theme,
|
|
1041
|
-
highlights: normalized.highlights,
|
|
1042
|
-
size: normalized.size,
|
|
1043
|
-
padding: normalized.padding,
|
|
1044
|
-
borderSize: normalized.borderSize,
|
|
1045
|
-
flipped: normalized.flipped,
|
|
1046
|
-
colors: normalized.colors,
|
|
1047
|
-
coordinates: normalized.coordinates
|
|
1048
|
-
});
|
|
1370
|
+
return renderer.render(request);
|
|
1049
1371
|
}
|
|
1050
1372
|
async toFile(filePath) {
|
|
1051
1373
|
const buffer = await this.toBuffer();
|
|
1052
1374
|
await writeBufferToFile(filePath, buffer);
|
|
1053
1375
|
}
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1376
|
+
async toSvg() {
|
|
1377
|
+
if (!this.position) {
|
|
1378
|
+
throw new ValidationError("No board position loaded");
|
|
1379
|
+
}
|
|
1380
|
+
const renderer = new SvgRenderer();
|
|
1381
|
+
const request = createRenderRequest(this.position, {
|
|
1382
|
+
...this.defaults,
|
|
1383
|
+
highlights: this.highlights,
|
|
1384
|
+
highlightSquares: void 0
|
|
1385
|
+
});
|
|
1386
|
+
return renderer.render(request);
|
|
1060
1387
|
}
|
|
1061
|
-
|
|
1062
|
-
|
|
1388
|
+
async toSvgFile(filePath) {
|
|
1389
|
+
await writeStringToFile(filePath, await this.toSvg());
|
|
1063
1390
|
}
|
|
1064
|
-
|
|
1065
|
-
|
|
1391
|
+
async toJpeg() {
|
|
1392
|
+
if (!this.position) {
|
|
1393
|
+
throw new ValidationError("No board position loaded");
|
|
1394
|
+
}
|
|
1395
|
+
const renderer = new CanvasJpegRenderer();
|
|
1396
|
+
const request = createRenderRequest(this.position, {
|
|
1397
|
+
...this.defaults,
|
|
1398
|
+
highlights: this.highlights,
|
|
1399
|
+
highlightSquares: void 0
|
|
1400
|
+
});
|
|
1401
|
+
return renderer.render(request);
|
|
1066
1402
|
}
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
async function renderChess(options) {
|
|
1070
|
-
const provided = [
|
|
1071
|
-
typeof options.fen === "string" ? options.fen : void 0,
|
|
1072
|
-
typeof options.pgn === "string" ? options.pgn : void 0,
|
|
1073
|
-
Array.isArray(options.board) ? options.board : void 0
|
|
1074
|
-
].filter((value) => value !== void 0);
|
|
1075
|
-
if (provided.length !== 1) {
|
|
1076
|
-
throw new ValidationError("Exactly one of fen, pgn, or board must be provided");
|
|
1403
|
+
async toJpegFile(filePath) {
|
|
1404
|
+
await writeBufferToFile(filePath, await this.toJpeg());
|
|
1077
1405
|
}
|
|
1078
|
-
|
|
1406
|
+
};
|
|
1407
|
+
|
|
1408
|
+
// src/api/functional-api.ts
|
|
1409
|
+
async function renderChess(options) {
|
|
1079
1410
|
const renderer = new CanvasPngRenderer();
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
1411
|
+
return renderer.render(createRenderRequestFromOptions(options));
|
|
1412
|
+
}
|
|
1413
|
+
async function renderSvg(options) {
|
|
1414
|
+
const renderer = new SvgRenderer();
|
|
1415
|
+
return renderer.render(createRenderRequestFromOptions(options));
|
|
1416
|
+
}
|
|
1417
|
+
async function renderJpeg(options) {
|
|
1418
|
+
const renderer = new CanvasJpegRenderer();
|
|
1419
|
+
return renderer.render(createRenderRequestFromOptions(options));
|
|
1420
|
+
}
|
|
1421
|
+
async function renderFile(filePath, options) {
|
|
1422
|
+
await writeBufferToFile(filePath, await renderChess(options));
|
|
1423
|
+
}
|
|
1424
|
+
async function renderSvgFile(filePath, options) {
|
|
1425
|
+
await writeStringToFile(filePath, await renderSvg(options));
|
|
1426
|
+
}
|
|
1427
|
+
async function renderJpegFile(filePath, options) {
|
|
1428
|
+
await writeBufferToFile(filePath, await renderJpeg(options));
|
|
1092
1429
|
}
|
|
1093
1430
|
export {
|
|
1094
1431
|
ChessImageGenerator,
|
|
@@ -1098,6 +1435,11 @@ export {
|
|
|
1098
1435
|
ThemeError,
|
|
1099
1436
|
ValidationError,
|
|
1100
1437
|
registerTheme,
|
|
1101
|
-
renderChess
|
|
1438
|
+
renderChess,
|
|
1439
|
+
renderFile,
|
|
1440
|
+
renderJpeg,
|
|
1441
|
+
renderJpegFile,
|
|
1442
|
+
renderSvg,
|
|
1443
|
+
renderSvgFile
|
|
1102
1444
|
};
|
|
1103
1445
|
//# sourceMappingURL=index.js.map
|