Skip to content

Commit f3a1699

Browse files
committed
#3104 Render images with resvg in issuebot
1 parent 629f176 commit f3a1699

File tree

6 files changed

+989
-753
lines changed

6 files changed

+989
-753
lines changed

.github/workflows/issuebot.yml

Lines changed: 50 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,14 +11,14 @@ jobs:
1111
outputs:
1212
sanityCheck: ${{ steps.sanity_check.outputs.message }}
1313
steps:
14-
- uses: actions/checkout@v3
14+
- uses: actions/checkout@v5
1515
with:
1616
token: ${{ secrets.RMG_BUILD_AND_RELEASE }}
1717
fetch-depth: 0 # Fetch all to get authors of every template.
1818

1919
- uses: actions/setup-node@v4
2020
with:
21-
node-version: '20'
21+
node-version: '24'
2222

2323
- run: npm ci
2424
working-directory: scripts
@@ -47,7 +47,7 @@ jobs:
4747
- name: Install Noto CJK fonts
4848
run: sudo apt install -y fonts-noto-cjk
4949

50-
- name: Make metadata and images
50+
- name: Make metadata and SVG
5151
run: node --loader ts-node/esm ./issuebot.ts
5252
working-directory: scripts
5353
env:
@@ -57,6 +57,53 @@ jobs:
5757
USER_ID: ${{ github.event.issue.user.id }}
5858
id: bot
5959

60+
- name: Download resvg
61+
run: |
62+
wget https://github.com/linebender/resvg/releases/download/v0.45.1/resvg-linux-x86_64.tar.gz
63+
tar -xzf resvg-linux-x86_64.tar.gz
64+
chmod +x resvg
65+
working-directory: scripts
66+
67+
- name: Clone RMP
68+
uses: actions/checkout@v5
69+
with:
70+
token: ${{ secrets.RMG_BUILD_AND_RELEASE }}
71+
repository: railmapgen/rmp
72+
path: rmp
73+
74+
- name: Clone RMT
75+
uses: actions/checkout@v5
76+
with:
77+
token: ${{ secrets.RMG_BUILD_AND_RELEASE }}
78+
repository: railmapgen/railmapgen.github.io
79+
path: rmt
80+
81+
- name: Copy fonts
82+
run: |
83+
mkdir -p fonts
84+
ls ../rmp/public/fonts/
85+
ls ../rmt/public/fonts/
86+
cp -r ../rmp/public/fonts/* ./fonts/
87+
cp ../rmt/public/fonts/*.ttf ./fonts/
88+
tree /usr/share/fonts
89+
echo "Copy system Noto CJK fonts for fallback"
90+
sudo find /usr/share/fonts -name "NotoSans*.ttf" -o -name "NotoSansCJK*.ttc" -o -name "NotoSerif*.ttf" -o -name "NotoSerifCJK*.ttc" | xargs -I {} sudo cp {} ./fonts/ || true
91+
ls fonts/
92+
working-directory: scripts
93+
94+
- name: Convert SVG to PNG using resvg
95+
run: |
96+
SVG_FILE=$(ls $HOME/Downloads/RMP_*.svg | head -n 1)
97+
CITY_NAME=$(basename "$SVG_FILE" .svg | sed 's/RMP_//')
98+
./resvg --skip-system-fonts --use-fonts-dir fonts --list-fonts
99+
./resvg --skip-system-fonts --use-fonts-dir fonts --sans-serif-family "Noto Sans CJK SC" --background white -z 2 "$SVG_FILE" "$HOME/Downloads/${CITY_NAME}.png"
100+
cp "$HOME/Downloads/${CITY_NAME}.png" ../public/resources/thumbnails/
101+
working-directory: scripts
102+
103+
- name: Generate thumbnail
104+
run: node --loader ts-node/esm ./make-thumbnail.ts
105+
working-directory: scripts
106+
60107
- name: Make logins
61108
run: |
62109
node ./loginbot.js

scripts/images.ts

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ import { resolve } from 'path';
77
import { Browser, Builder, By, Capabilities, until } from 'selenium-webdriver';
88
import sharp from 'sharp';
99

10-
export const makeImage = async (filePath: string) => {
10+
export const makeImage = async (filePath: string, svg = false) => {
1111
const capabilities = new Capabilities();
1212
capabilities.set('browserName', Browser.FIREFOX);
1313
// https://developer.mozilla.org/en-US/docs/Web/WebDriver/Capabilities/firefoxOptions
@@ -38,6 +38,13 @@ export const makeImage = async (filePath: string) => {
3838
"//button[contains(@class, 'chakra-menu__menuitem')][starts-with(@id, 'menu-list-download-menuitem-')][3]";
3939
await driver.findElement(By.xpath(exportImageButtonXPath)).click();
4040

41+
if (svg) {
42+
const dropdownXpath = '/html/body/div[7]/div[3]/div/section/div/div[1]/div/div/select';
43+
await driver.findElement(By.xpath(dropdownXpath)).click();
44+
const option2Xpath = '/html/body/div[7]/div[3]/div/section/div/div[1]/div/div/select/option[2]';
45+
await driver.findElement(By.xpath(option2Xpath)).click();
46+
}
47+
4148
driver.findElement(By.xpath('/html/body/div[7]/div[3]/div/section/div/div[3]/label/span[2]')).click();
4249
driver.findElement(By.xpath('/html/body/div[7]/div[3]/div/section/div/label[2]/span[1]')).click();
4350

@@ -47,9 +54,9 @@ export const makeImage = async (filePath: string) => {
4754
let retry = 0;
4855
while (retry < 3) {
4956
retry += 1;
50-
await new Promise(r => setTimeout(r, 20000));
57+
await new Promise(r => setTimeout(r, svg ? 5000 : 20000));
5158
const files = await readdir(resolve(homedir(), 'Downloads'));
52-
const filename = files.find(s => s.startsWith('RMP_') && s.endsWith('.png'));
59+
const filename = files.find(s => s.startsWith('RMP_') && s.endsWith(svg ? '.svg' : '.png'));
5360
if (!filename) continue;
5461

5562
await driver.quit();

scripts/issuebot.ts

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -80,10 +80,8 @@ export const main = async () => {
8080
if (!existsSync(resolve('..', 'public', 'resources', 'thumbnails')))
8181
await mkdir(resolve('..', 'public', 'resources', 'thumbnails'));
8282
if (!existsSync(resolve(homedir(), 'Downloads'))) await mkdir(resolve(homedir(), 'Downloads'));
83-
const image = await makeImage(resolve('..', 'public', 'resources', type, `${cityName}.json`));
84-
await writeFile(resolve('..', 'public', 'resources', 'thumbnails', `${cityName}.png`), image);
85-
const thumbnail = await makeThumbnail(image);
86-
await writeFile(resolve('..', 'public', 'resources', 'thumbnails', `${cityName}@300.png`), thumbnail);
83+
const imageSVG = await makeImage(resolve('..', 'public', 'resources', type, `${cityName}.json`), true);
84+
await writeFile(resolve(homedir(), 'Downloads', `RMP_${cityName}.svg`), imageSVG);
8785

8886
execSync(`git checkout -b bot-${process.env.ISSUE_NUMBER}`);
8987

scripts/make-thumbnail.ts

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
import { readFile, writeFile, readdir } from 'fs/promises';
2+
import { resolve } from 'path';
3+
import { homedir } from 'os';
4+
import sharp from 'sharp';
5+
6+
const makeThumbnail = async (image: Buffer): Promise<Buffer> => {
7+
const img = sharp(image, { limitInputPixels: 1024000000 });
8+
const metadata = await img.metadata();
9+
const width = Math.floor((metadata.width! * 2) / 3);
10+
const height = Math.floor((metadata.height! * 2) / 3);
11+
const sideLength = Math.min(width, height);
12+
const left = Math.floor((metadata.width! - sideLength) / 2);
13+
const top = Math.floor((metadata.height! - sideLength) / 2);
14+
return await img.extract({ width: sideLength, height: sideLength, left, top }).resize(300, 300).toBuffer();
15+
};
16+
17+
const directory = resolve(homedir(), 'Downloads');
18+
const files = await readdir(directory);
19+
const pngFile = files.find(s => s.endsWith('.png') && !s.includes('@300'));
20+
if (!pngFile) throw new Error('PNG file not found');
21+
22+
const cityName = pngFile.replace('.png', '');
23+
const image = await readFile(resolve(directory, pngFile));
24+
const thumbnail = await makeThumbnail(image);
25+
await writeFile(resolve('..', 'public', 'resources', 'thumbnails', `${cityName}@300.png`), thumbnail);
26+
console.log('Thumbnail generated successfully');

0 commit comments

Comments
 (0)