build(sdlc): seed data, figures and rendered package for 23/09 feedback
This commit is contained in:
@@ -144,3 +144,8 @@ table.signature-table th:nth-child(4), table.signature-table td:nth-child(4) { w
|
||||
reference package, so they take a smaller face to fit the text block. */
|
||||
table.dense { font-size: 7.2pt; }
|
||||
table.dense th, table.dense td { padding: 1mm 1.2mm; }
|
||||
|
||||
/* Figures in the Software Design: full column width, caption under the image. */
|
||||
.delivery-document figure.figure { margin: 8pt 0 12pt; text-align: center; break-inside: avoid; page-break-inside: avoid; }
|
||||
.delivery-document figure.figure img { max-width: 100%; max-height: 225mm; border: .5pt solid #bfbfbf; }
|
||||
.delivery-document figure.figure figcaption { margin-top: 4pt; font-size: 9.5pt; font-weight: 700; }
|
||||
|
||||
@@ -188,15 +188,19 @@ LOGO_MIN_PX = 100
|
||||
def strip_logo_images(document):
|
||||
"""Drop the letterhead logo left loose in the body.
|
||||
|
||||
The work products carry no figures of their own, but the converter draws
|
||||
each bullet as a small image, so only the logo is removed - anything that
|
||||
size would be furniture, anything smaller is a list marker.
|
||||
The converter draws each bullet as a small image, so only the logo is
|
||||
removed: it is square and at least LOGO_MIN_PX, whereas anything smaller is
|
||||
a list marker. The Software Design figures are wider than they are tall,
|
||||
so they are kept.
|
||||
"""
|
||||
def is_logo(image):
|
||||
width, height = image.px_width, image.px_height
|
||||
return max(width, height) >= LOGO_MIN_PX and 0.8 <= width / max(height, 1) <= 1.25
|
||||
|
||||
logos = {
|
||||
relationship_id
|
||||
for relationship_id, part in document.part.related_parts.items()
|
||||
if getattr(part, 'image', None) is not None
|
||||
and max(part.image.px_width, part.image.px_height) >= LOGO_MIN_PX
|
||||
if getattr(part, 'image', None) is not None and is_logo(part.image)
|
||||
}
|
||||
|
||||
removed = 0
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
// Rasterise the Software Design figures (scripts/sdlc-seed/figures.mjs) to PNG with the
|
||||
// document font, so Thai labels print the same in the PDF and survive the Word conversion.
|
||||
// Usage: node scripts/sdlc-delivery/render-figures.mjs
|
||||
import fs from 'node:fs/promises';
|
||||
import path from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import { chromium } from 'playwright';
|
||||
import { FIGURES } from '../sdlc-seed/figures.mjs';
|
||||
|
||||
const here = path.dirname(fileURLToPath(import.meta.url));
|
||||
const output = path.resolve(here, '../sdlc-seed/figures');
|
||||
|
||||
async function main() {
|
||||
const font = await fs.readFile(path.join(here, 'assets/LeelawadeeUI.ttf'));
|
||||
const fontFace = `@font-face { font-family: "BRN Thai"; src: url(data:font/ttf;base64,${font.toString('base64')}) format("truetype"); font-weight: 400 700; }`;
|
||||
await fs.mkdir(output, { recursive: true });
|
||||
const browser = await chromium.launch({ headless: true });
|
||||
try {
|
||||
const page = await (await browser.newContext({ deviceScaleFactor: 2 })).newPage();
|
||||
for (const [file, , draw] of Object.values(FIGURES)) {
|
||||
const svg = draw();
|
||||
const [, w, h] = svg.match(/width="(\d+)" height="(\d+)"/);
|
||||
await page.setViewportSize({ width: Number(w), height: Number(h) });
|
||||
await page.setContent(`<!doctype html><html><head><meta charset="utf-8"><style>${fontFace} html,body{margin:0}</style></head><body>${svg}</body></html>`);
|
||||
await page.evaluate(() => document.fonts.ready);
|
||||
await page.locator('svg').first().screenshot({ path: path.join(output, `${file}.png`) });
|
||||
console.log(`Rendered figures/${file}.png`);
|
||||
}
|
||||
} finally {
|
||||
await browser.close();
|
||||
}
|
||||
}
|
||||
|
||||
main().catch((error) => { console.error(error); process.exit(1); });
|
||||
@@ -1,4 +1,5 @@
|
||||
import fs from 'node:fs/promises';
|
||||
import { readFileSync } from 'node:fs';
|
||||
import path from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import { chromium } from 'playwright';
|
||||
@@ -101,7 +102,7 @@ function renderParagraph(lines) {
|
||||
}).join('');
|
||||
}
|
||||
|
||||
function markdownToHtml(markdown) {
|
||||
function markdownToHtml(markdown, baseDir) {
|
||||
const lines = markdown.replaceAll('\r\n', '\n').split('\n');
|
||||
const output = [];
|
||||
let index = 0;
|
||||
@@ -110,6 +111,15 @@ function markdownToHtml(markdown) {
|
||||
const line = lines[index];
|
||||
if (!line.trim()) { index += 1; continue; }
|
||||
|
||||
// A figure: , embedded so the PDF needs no external files.
|
||||
const image = line.match(/^!\[([^\]]*)\]\(([^)]+)\)\s*$/);
|
||||
if (image) {
|
||||
const data = readFileSync(path.resolve(baseDir, image[2])).toString('base64');
|
||||
output.push(`<figure class="figure"><img src="data:image/png;base64,${data}" alt=""><figcaption>${renderInline(image[1])}</figcaption></figure>`);
|
||||
index += 1;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (isTableLine(line)) {
|
||||
const table = [];
|
||||
while (index < lines.length && isTableLine(lines[index])) table.push(lines[index++]);
|
||||
@@ -149,7 +159,7 @@ function markdownToHtml(markdown) {
|
||||
}
|
||||
|
||||
const paragraph = [];
|
||||
while (index < lines.length && lines[index].trim() && !isTableLine(lines[index]) && !/^(#{1,3})\s+/.test(lines[index]) && !/^[-*]\s+/.test(lines[index]) && !/^\d+\.\s+/.test(lines[index])) paragraph.push(lines[index++]);
|
||||
while (index < lines.length && lines[index].trim() && !isTableLine(lines[index]) && !/^!\[/.test(lines[index]) && !/^(#{1,3})\s+/.test(lines[index]) && !/^[-*]\s+/.test(lines[index]) && !/^\d+\.\s+/.test(lines[index])) paragraph.push(lines[index++]);
|
||||
const isApproval = paragraph.some((line) => /^(Name|Role|Signature|Date|Position|Company|Project roles|Decision):/.test(line.trimEnd()));
|
||||
const html = `<p${isApproval ? ' class="approval-fields"' : ''}>${renderParagraph(paragraph)}</p>`;
|
||||
// Keep a signatory's heading and fields on one page.
|
||||
@@ -226,7 +236,7 @@ async function main() {
|
||||
? '@page { size: A4 landscape; margin: 32mm 15mm 24mm 15mm; }'
|
||||
: '';
|
||||
const templateFont = `<style>${fontFace}</style>`;
|
||||
const html = `<!doctype html><html><head><meta charset="utf-8"><style>${fontFace}${css}${pageLayout}</style></head><body><main class="delivery-document">${markdownToHtml(body)}</main></body></html>`;
|
||||
const html = `<!doctype html><html><head><meta charset="utf-8"><style>${fontFace}${css}${pageLayout}</style></head><body><main class="delivery-document">${markdownToHtml(body, path.dirname(source))}</main></body></html>`;
|
||||
|
||||
await fs.mkdir(path.dirname(destination), { recursive: true });
|
||||
const browser = await chromium.launch({ headless: true });
|
||||
|
||||
@@ -12,6 +12,8 @@ function extractDocumentContent(markdown) {
|
||||
.slice((titleMatch?.index ?? 0) + (titleMatch?.[0].length ?? 0))
|
||||
.replace(/^<!--\s*footer:.*?-->\s*$/m, '')
|
||||
.replace(/<br\s*\/?>/gi, ' ') // markup, not document text
|
||||
// A figure prints its caption; the image path is markup.
|
||||
.replace(/^!\[([^\]]*)\]\([^)]*\)\s*$/gm, '$1')
|
||||
// List markers are drawn by the renderer, not stored as text: the browser
|
||||
// paints them from <ol>/<ul> and Word from numbering.xml, so they are
|
||||
// markup here too and must not be expected in the extracted text.
|
||||
|
||||
Reference in New Issue
Block a user