Fix: improve PDF-output functionality

This commit is contained in:
Pieter Vander Vennet 2023-06-07 00:14:20 +02:00
parent c6283ac720
commit 215286a5af
22 changed files with 363 additions and 221 deletions

View file

@ -16,7 +16,7 @@
export let extension: string
export let mimetype: string
export let construct: (geojsonCleaned: FeatureCollection, title: string) => (Blob | string) | Promise<void>
export let construct: (geojsonCleaned: FeatureCollection, title: string, status?: UIEventSource<string>) => (Blob | string) | Promise<void>
export let mainText: Translation
export let helperText: Translation
export let metaIsIncluded: boolean
@ -43,7 +43,7 @@
const name = state.layout.id
const title = `MapComplete_${name}_export_${new Date().toISOString().substr(0, 19)}.${extension}`
const promise = construct(geojson, title)
const promise = construct(geojson, title, status)
let data: Blob | string
if (typeof promise === "string") {
data = promise
@ -88,7 +88,7 @@
{:else}
<button class="flex w-full" on:click={clicked}>
<slot name="image">
<ArrowDownTrayIcon class="w-12 h-12 mr-2"/>
<ArrowDownTrayIcon class="w-12 h-12 mr-2 shrink-0"/>
</slot>
<span class="flex flex-col items-start">
<Tr t={mainText}/>

View file

@ -48,7 +48,7 @@ export default class DownloadHelper {
public getCleanGeoJson(
includeMetaData: boolean
): string | FeatureCollection {
): FeatureCollection {
const featuresPerLayer = this.getCleanGeoJsonPerLayer(includeMetaData)
const features = [].concat(...Array.from(featuresPerLayer.values()))
return {
@ -179,4 +179,24 @@ export default class DownloadHelper {
return featuresPerLayer
}
/**
* Creates an image for the given key.
* @param key
* @param width (in mm)
* @param height (in mm)
*/
createImage(key: string, width: string, height: string): HTMLImageElement {
const img = document.createElement("img")
const sources = {
"layouticon":this._state.layout.icon
}
img.src = sources[key]
if(!img.src){
throw "Invalid key for 'createImage': "+key+"; try one of: "+Object.keys(sources).join(", ")
}
img.style.width = width
img.style.height = height
console.log("Fetching an image with src", img.src)
return img;
}
}

View file

@ -87,7 +87,7 @@
<div class="flex flex-col">
{#each Object.keys(SvgToPdf.templates) as key}
{#if SvgToPdf.templates[key].isPublic}
<DownloadPdf {state} templateName={key}></DownloadPdf>
<DownloadPdf {state} templateName={key}/>
{/if}
{/each}
</div>

View file

@ -3,6 +3,7 @@
import DownloadButton from "./DownloadButton.svelte";
import ThemeViewState from "../../Models/ThemeViewState";
import {SvgToPdf} from "../../Utils/svgToPdf";
import type {PdfTemplateInfo} from "../../Utils/svgToPdf";
import Translations from "../i18n/Translations";
import {Translation} from "../i18n/Translation";
import {Utils} from "../../Utils";
@ -13,45 +14,50 @@
import DownloadHelper from "./DownloadHelper";
export let templateName: string
export let state: ThemeViewState
const template = SvgToPdf.templates[templateName]
console.log("template", template )
let mainText: Translation = typeof template.description === "string" ? new Translation(template.description) : template.description
let t = Translations.t.general.download
export let state: ThemeViewState
const template: PdfTemplateInfo = SvgToPdf.templates[templateName]
console.log("template", template)
let mainText: Translation = typeof template.description === "string" ? new Translation(template.description) : template.description
let t = Translations.t.general.download
const downloadHelper = new DownloadHelper(state)
async function constructPdf(_, title: string, status: UIEventSource<string>) {
const templateUrls = SvgToPdf.templates["current_view_a3"].pages
const templates: string[] = await Promise.all(templateUrls.map(url => Utils.download(url)))
console.log("Templates are", templates)
const bg = state.mapProperties.rasterLayer.data ?? AvailableRasterLayers.maplibre
const creator = new SvgToPdf(title, templates, {
state,
freeComponentId: "belowmap",
generateImage(key: string): HTMLImageElement {
downloadHelper.generateImage(key)
},
textSubstitutions: <Record<string, string>> {
"layout.title": state.layout.title,
title: state.layout.title,
layoutImg: state.layout.icon,
version: Constants.vNumber,
date: new Date().toISOString().substring(0,16),
background: new Translation(bg.properties.name).txt
}
})
const unsub = creator.status.addCallbackAndRunD(s => status?.setData(s))
await creator.ExportPdf(Locale.language.data)
unsub()
return undefined
}
async function constructPdf(_, title: string, status: UIEventSource<string>) {
title=title.substring(0, title.length - 4)+"_"+template.format+"_"+template.orientation
const templateUrls = SvgToPdf.templates[templateName].pages
const templates: string[] = await Promise.all(templateUrls.map(url => Utils.download(url)))
console.log("Templates are", templates)
const bg = state.mapProperties.rasterLayer.data ?? AvailableRasterLayers.maplibre
const creator = new SvgToPdf(title, templates, {
state,
freeComponentId: "belowmap",
createImage: (key: string, width: string, height: string) => downloadHelper.createImage(key, width, height),
textSubstitutions: <Record<string, string>>{
"layout.title": state.layout.title,
layoutid: state.layout.id,
title: state.layout.title,
layoutImg: state.layout.icon,
version: Constants.vNumber,
date: new Date().toISOString().substring(0, 16),
background: new Translation(bg.properties.name).txt
}
})
const unsub = creator.status.addCallbackAndRunD(s => {
console.log("SVG creator status:", s)
status?.setData(s);
})
await creator.ExportPdf(Locale.language.data)
unsub()
return undefined
}
</script>
<DownloadButton {state}
mimetype="application/pdf"
<DownloadButton construct={constructPdf}
extension="pdf"
{mainText}
helperText={t.downloadAsPdfHelper}
construct={constructPdf}
metaIsIncluded={false}
{mainText}
mimetype="application/pdf"
{state}
/>

View file

@ -43,12 +43,13 @@ export class PngMapCreator {
const l = settings.location.data
document.getElementById(freeComponentId).appendChild(div)
const pixelRatio = 4
const mapElem = new MlMap({
container: div.id,
style: AvailableRasterLayers.maplibre.properties.url,
center: [l.lon, l.lat],
zoom: settings.zoom.data,
pixelRatio: 6
pixelRatio
});
const map = new UIEventSource<MlMap>(mapElem)
@ -71,11 +72,11 @@ export class PngMapCreator {
await Utils.waitFor(250)
}
// Some extra buffer...
setState("One second pause to make sure all images are loaded...")
await Utils.waitFor(1000)
setState("Exporting png")
console.log("Loading for", this._state.layout.id, "is done")
const png = await mla.exportAsPng(6)
return png
const dpiFactor = 1
setState("Exporting png (" + this._options.width + "mm * " + this._options.height + "mm , dpiFactor:" + dpiFactor + ", maplibre-canvas-pixelratio: " + pixelRatio + ")")
return await mla.exportAsPng(dpiFactor)
} finally {
div.parentElement.removeChild(div)
}

View file

@ -9,7 +9,6 @@ import {makeAbsolute, parseSVG} from "svg-path-parser"
import Translations from "../UI/i18n/Translations"
import {Utils} from "../Utils"
import Constants from "../Models/Constants"
import Hash from "../Logic/Web/Hash"
import ThemeViewState from "../Models/ThemeViewState"
import {Store, UIEventSource} from "../Logic/UIEventSource"
@ -22,19 +21,17 @@ class SvgToPdfInternals {
private currentMatrix: Matrix
private currentMatrixInverted: Matrix
private readonly _images: Record<string, HTMLImageElement>
private readonly _rects: Record<string, SVGRectElement>
private readonly extractTranslation: (string) => string
private readonly page: SvgToPdfPage;
private readonly usedRectangles = new Set<string>()
constructor(
advancedApi: jsPDF,
images: Record<string, HTMLImageElement>,
rects: Record<string, SVGRectElement>,
page: SvgToPdfPage,
extractTranslation: (string) => string
) {
this.page = page;
this.doc = advancedApi
this._images = images
this._rects = rects
this.extractTranslation = (s) => extractTranslation(s)?.replace(/&nbsp;/g, " ")
this.currentMatrix = this.doc.unitMatrix
this.currentMatrixInverted = this.doc.unitMatrix
@ -55,7 +52,6 @@ class SvgToPdfInternals {
if (translateMatch !== null) {
const dx = Number(translateMatch[1])
const dy = Number(translateMatch[2])
console.log("Translating", dx, dy)
return SvgToPdfInternals.dummyDoc.Matrix(1, 0, 0, 1, dx, dy)
}
@ -211,6 +207,7 @@ class SvgToPdfInternals {
public handleElement(element: SVGSVGElement | Element): void {
const isTransformed = this.setTransform(element)
this.page.status.set("Handling element "+element.tagName+" "+element.id)
try {
if (element.tagName === "tspan") {
if (element.childElementCount == 0) {
@ -237,7 +234,9 @@ class SvgToPdfInternals {
}
if (element.tagName === "rect") {
this.drawRect(<any>element)
if (!this.usedRectangles.has(element.id)) {
this.drawRect(<SVGRectElement>element)
}
}
if (element.tagName === "circle") {
@ -267,7 +266,6 @@ class SvgToPdfInternals {
opacity = Number(css["fill-opacity"])
this.doc.setGState(this.doc.GState({opacity: opacity}))
}
console.log("Fill color is:", color, opacity)
this.doc.setFillColor(color)
this.doc.roundedRect(x, y, width, height, rx, ry, "F")
@ -308,8 +306,46 @@ class SvgToPdfInternals {
if (txt == "") {
return
}
const x = SvgToPdfInternals.attrNumber(tspan, "x")
const y = SvgToPdfInternals.attrNumber(tspan, "y")
let x = SvgToPdfInternals.attrNumber(tspan, "x")
let y = SvgToPdfInternals.attrNumber(tspan, "y")
const m = SvgToPdfInternals.extractMatrix(tspan.parentElement)
const p = m?.inversed()?.applyToPoint({x, y})
x = p?.x ?? x
y = p?.y ?? y
const imageMatch = txt.match(/^\$img\(([^)]*)\)$/)
if (imageMatch) {
// We want to draw a special image
const [_, key] = imageMatch
console.log("Creating image with key", key, "searching rect in", x, y)
const rectangle: SVGRectElement = this.page.findSmallestRectContaining(x, y, false)
console.log("Got rect", rectangle)
let w = SvgToPdfInternals.attrNumber(rectangle, "width")
let h = SvgToPdfInternals.attrNumber(rectangle, "height")
x = SvgToPdfInternals.attrNumber(rectangle, "x")
y = SvgToPdfInternals.attrNumber(rectangle, "y")
// Actually, dots per mm, not dots per inch ;)
let dpi = 60
const img = this.page.options.createImage(key, dpi * w + "px", dpi * h + "px")
const canvas = document.createElement("canvas")
const ctx = canvas.getContext("2d")
canvas.width = w * dpi
canvas.height = h * dpi
img.style.width = `${w * dpi}px`
img.style.height = `${h * dpi}px`
ctx.drawImage(img, 0, 0, w * dpi, h * dpi)
const base64img = canvas.toDataURL("image/png")
// Don't ask me why this magicFactor transformation is needed - but it works
const magicFactor = 3.8
this.addMatrix(this.doc.Matrix(1 / magicFactor, 0, 0, 1 / magicFactor, 0, 0))
this.doc.addImage(base64img, "png", x, y, w, h)
this.undoTransform()
this.usedRectangles.add(rectangle.id)
return
}
let maxWidth: number = undefined
let maxHeight: number = undefined
@ -319,9 +355,11 @@ class SvgToPdfInternals {
const matched = css["shape-inside"].match(/url\(#([a-zA-Z0-9-]+)\)/)
if (matched !== null) {
const rectId = matched[1]
const rect = this._rects[rectId]
maxWidth = SvgToPdfInternals.attrNumber(rect, "width", false)
maxHeight = SvgToPdfInternals.attrNumber(rect, "height", false)
const rect = this.page.rects[rectId]?.rect
if (rect) {
maxWidth = SvgToPdfInternals.attrNumber(rect, "width", false)
maxHeight = SvgToPdfInternals.attrNumber(rect, "height", false)
}
}
}
@ -375,7 +413,6 @@ class SvgToPdfInternals {
const list = text.match(/\$list\(([a-zA-Z0-9_.-]+)\)/)
if (list) {
const key = list[1]
console.log("Generating a list with key" + key)
let r = this.extractTranslation("$" + key + "0")
let i = 0
result += "\n"
@ -395,13 +432,15 @@ class SvgToPdfInternals {
addSpace = true
}
}
const options = {}
if (maxWidth) {
options["maxWidth"] = maxWidth
}
this.doc.text(
result,
x,
y,
{
maxWidth,
},
options,
this.currentMatrix
)
}
@ -409,8 +448,8 @@ class SvgToPdfInternals {
private drawSvgViaCanvas(element: Element): void {
const x = SvgToPdfInternals.attrNumber(element, "x")
const y = SvgToPdfInternals.attrNumber(element, "y")
const width = SvgToPdfInternals.attrNumber(element, "width")
const height = SvgToPdfInternals.attrNumber(element, "height")
const width = SvgToPdfInternals.attrNumber(element, "width")
const base64src = SvgToPdfInternals.attr(element, "xlink:href")
const svgXml = atob(base64src.substring(base64src.indexOf(";base64,") + ";base64,".length))
const parser = new DOMParser()
@ -419,7 +458,7 @@ class SvgToPdfInternals {
const svgWidth = SvgToPdfInternals.attrNumber(svgRoot, "width")
const svgHeight = SvgToPdfInternals.attrNumber(svgRoot, "height")
let img = this._images[base64src]
let img = this.page.images[base64src]
// This is an svg image, we use the canvas to convert it to a png
const canvas = document.createElement("canvas")
const ctx = canvas.getContext("2d")
@ -530,25 +569,29 @@ export interface SvgToPdfOptions {
* Override all the maps to generate with this map
*/
state?: ThemeViewState,
createImage(key: string, width: string, height: string): HTMLImageElement
}
class SvgToPdfPage {
public readonly _svgRoot: SVGSVGElement
private images: Record<string, HTMLImageElement> = {}
private rects: Record<string, SVGRectElement> = {}
images: Record<string, HTMLImageElement> = {}
rects: Record<string, { rect: SVGRectElement, isInDef: boolean }> = {}
readonly options: SvgToPdfOptions
private readonly importedTranslations: Record<string, string> = {}
private readonly layerTranslations: Record<string, Record<string, any>> = {}
private readonly options: SvgToPdfOptions
/**
* Small indicator for humans
* @private
*/
private readonly _state: UIEventSource<string>
private _isPrepared = false
public readonly status: UIEventSource<string>;
constructor(page: string, state: UIEventSource<string>, options: SvgToPdfOptions) {
constructor(page: string, state: UIEventSource<string>, options: SvgToPdfOptions, status: UIEventSource<string>) {
this._state = state
this.options = options
this.status = status;
const parser = new DOMParser()
const xmlDoc = parser.parseFromString(page, "image/svg+xml")
this._svgRoot = xmlDoc.getElementsByTagName("svg")[0]
@ -567,7 +610,6 @@ class SvgToPdfPage {
(t) => t.textContent
)
const translations = new Set<string>()
console.log("Extracting translations, contents are", textContents)
for (const tc of textContents) {
const parts = tc.split(" ").filter((p) => p.startsWith("$") && p.indexOf("(") < 0)
for (let part of parts) {
@ -581,21 +623,19 @@ class SvgToPdfPage {
}
}
}
console.log("Translations keys are", translations)
return translations
}
/**
* Does some preparatory work, most importantly gathering the map specifications into parameter `mapTextSpecs` and substituting translations
* @param element
* @param mapTextSpecs
*/
public async prepareElement(
element: SVGSVGElement | Element,
mapTextSpecs: SVGTSpanElement[]
mapTextSpecs: SVGTSpanElement[],
inDefs: boolean
): Promise<void> {
if (element.tagName === "rect") {
this.rects[element.id] = <SVGRectElement>element
this.rects[element.id] = {rect: <SVGRectElement>element, isInDef: inDefs}
}
if (element.tagName === "image") {
await this.loadImage(element)
@ -612,17 +652,13 @@ class SvgToPdfPage {
const [, pathRaw, as] = importMatch
this.importedTranslations[as] = pathRaw
}
const setPropertyMatch = element.textContent.match(
/\$set\(([a-zA-Z-_0-9.?:]+),(.+)\)/
)
if (setPropertyMatch) {
this.options.textSubstitutions[setPropertyMatch[1].trim()] =
setPropertyMatch[2].trim()
console.log(
"Setting a property:",
setPropertyMatch,
this.options.textSubstitutions
)
}
if (element.textContent.startsWith("$map(")) {
@ -638,7 +674,7 @@ class SvgToPdfPage {
element.tagName === "defs"
) {
for (let child of Array.from(element.children)) {
await this.prepareElement(child, mapTextSpecs)
await this.prepareElement(child, mapTextSpecs, inDefs || element.tagName === "defs")
}
}
}
@ -667,16 +703,11 @@ class SvgToPdfPage {
this._isPrepared = true
const mapSpecs: SVGTSpanElement[] = []
for (let child of Array.from(this._svgRoot.children)) {
await this.prepareElement(<any>child, mapSpecs)
await this.prepareElement(<any>child, mapSpecs, child.tagName === "defs")
}
for (const mapSpec of mapSpecs) {
try {
await this.prepareMap(mapSpec, !this.options?.disableDataLoading)
} catch (e) {
console.error("Couldn't prepare a map:", e)
}
await this.prepareMap(mapSpec, !this.options?.disableDataLoading)
}
}
@ -689,12 +720,13 @@ class SvgToPdfPage {
this.options.beforePage(i)
}
const self = this
const internal = new SvgToPdfInternals(advancedApi, this.images, this.rects, (key) =>
self.extractTranslation(key, language)
const internal = new SvgToPdfInternals(advancedApi, this, (key) =>
self.extractTranslation(key, language)
)
for (let child of Array.from(this._svgRoot.children)) {
internal.handleElement(<any>child)
}
}
extractTranslation(text: string, language: string, strict: boolean = false) {
@ -753,6 +785,33 @@ class SvgToPdfPage {
}
}
public findSmallestRectContaining(x: number, y: number, shouldBeInDefinitionSection: boolean) {
let smallestRect: SVGRectElement = undefined
let smallestSurface: number = undefined
// We iterate over all the rectangles and pick the smallest (by surface area) that contains the upper left point of the tspan
for (const id in this.rects) {
const {rect, isInDef} = this.rects[id]
if (shouldBeInDefinitionSection !== isInDef) {
continue
}
const rx = SvgToPdfInternals.attrNumber(rect, "x")
const ry = SvgToPdfInternals.attrNumber(rect, "y")
const w = SvgToPdfInternals.attrNumber(rect, "width")
const h = SvgToPdfInternals.attrNumber(rect, "height")
const inBounds = rx <= x && x <= rx + w && ry <= y && y <= ry + h
if (!inBounds) {
continue
}
const surface = w * h
if (smallestSurface === undefined || smallestSurface > surface) {
smallestSurface = surface
smallestRect = rect
}
}
return smallestRect
}
private loadImage(element: Element | string): Promise<void> {
const xlink = typeof element === "string" ? element : element.getAttribute("xlink:href")
let img = document.createElement("img")
@ -805,27 +864,7 @@ class SvgToPdfPage {
}
const spec = textElement.textContent
let smallestRect: SVGRectElement = undefined
let smallestSurface: number = undefined
// We iterate over all the rectangles and pick the smallest (by surface area) that contains the upper left point of the tspan
for (const id in this.rects) {
const rect = this.rects[id]
const rx = SvgToPdfInternals.attrNumber(rect, "x")
const ry = SvgToPdfInternals.attrNumber(rect, "y")
const w = SvgToPdfInternals.attrNumber(rect, "width")
const h = SvgToPdfInternals.attrNumber(rect, "height")
const inBounds = rx <= x && x <= rx + w && ry <= y && y <= ry + h
if (!inBounds) {
console.log("Not in bounds: rectangle", id)
continue
}
const surface = w * h
if (smallestSurface === undefined || smallestSurface > surface) {
smallestSurface = surface
smallestRect = rect
}
}
const smallestRect = this.findSmallestRectContaining(x, y, false)
if (smallestRect === undefined) {
throw (
"No rectangle found around " +
@ -833,7 +872,6 @@ class SvgToPdfPage {
". Draw a rectangle around it, the map will be projected on that one"
)
}
const svgImage = document.createElement("image")
svgImage.setAttribute("x", smallestRect.getAttribute("x"))
svgImage.setAttribute("y", smallestRect.getAttribute("y"))
@ -880,8 +918,6 @@ class SvgToPdfPage {
}
const zoom = Number(params["zoom"] ?? params["z"] ?? 14)
Hash.hash.setData(undefined)
// QueryParameters.ClearAll()
const state = new ThemeViewState(layout)
state.mapProperties.location.setData({
lat: this.options?.overrideLocation?.lat ?? Number(params["lat"] ?? 51.05016),
@ -889,8 +925,6 @@ class SvgToPdfPage {
})
state.mapProperties.zoom.setData(zoom)
console.log("Params are", params, params["layers"] === "none")
const fl = Array.from(state.layerState.filteredLayers.values())
for (const filteredLayer of fl) {
if (params["layer-" + filteredLayer.layerDef.id] !== undefined) {
@ -937,7 +971,6 @@ class SvgToPdfPage {
}
}
}
console.log("Creating a map width ", width, height, params.scalingFactor)
const pngCreator = new PngMapCreator(state, {
width: 4 * width,
height: 4 * height,
@ -950,7 +983,7 @@ class SvgToPdfPage {
svgImage.setAttribute("xlink:href", await SvgToPdfPage.blobToBase64(png))
smallestRect.parentElement.insertBefore(svgImage, smallestRect)
await this.prepareElement(svgImage, [])
await this.prepareElement(svgImage, [], false)
const smallestRectCss = SvgToPdfInternals.parseCss(smallestRect.getAttribute("style"))
smallestRectCss["fill-opacity"] = "0"
smallestRect.setAttribute(
@ -964,36 +997,51 @@ class SvgToPdfPage {
}
}
export interface PdfTemplateInfo{ pages: string[];
description: string | Translation;
format: "a3" | "a4" | "a2",
orientation: "portrait" | "landscape"
isPublic: boolean }
export class SvgToPdf {
public static readonly templates: Record<
"flyer_a4" | "poster_a3" | "poster_a2" | "current_view_a4" | "current_view_a3",
{ pages: string[]; description: string | Translation; isPublic: boolean }
PdfTemplateInfo
> = {
flyer_a4: {
pages: [
"./assets/templates/MapComplete-flyer.svg",
"./assets/templates/MapComplete-flyer.back.svg",
],
format: "a4",
orientation: "landscape",
description: Translations.t.flyer.description,
isPublic: false
},
poster_a3: {
format: "a3",
orientation: "portrait",
pages: ["./assets/templates/MapComplete-poster-a3.svg"],
description: "A basic A3 poster (similar to the flyer)",
isPublic: false
},
poster_a2: {
format: "a2",
orientation: "portrait",
pages: ["./assets/templates/MapComplete-poster-a2.svg"],
description: "A basic A2 poster (similar to the flyer); scaled up from the A3 poster",
isPublic: false
},
current_view_a4: {
format: "a4",
orientation: "landscape",
pages: ["./assets/templates/CurrentMapWithHeaderA4.svg"],
description: Translations.t.general.download.pdf.current_view_a4,
isPublic: true
},
current_view_a3: {
format: "a3",
orientation: "portrait",
pages: ["./assets/templates/CurrentMapWithHeaderA3.svg"],
description: Translations.t.general.download.pdf.current_view_a3,
isPublic: true
@ -1004,7 +1052,7 @@ export class SvgToPdf {
private readonly _title: string
private readonly _pages: SvgToPdfPage[]
constructor(title: string, pages: string[], options) {
constructor(title: string, pages: string[], options: SvgToPdfOptions) {
this._title = title
options.textSubstitutions = options.textSubstitutions ?? {}
options.textSubstitutions["mapCount"] = "" +
@ -1015,7 +1063,7 @@ export class SvgToPdf {
const state = new UIEventSource<string>("Initializing...")
this.status = state
this._status = state
this._pages = pages.map((page) => new SvgToPdfPage(page, state, options))
this._pages = pages.map((page) => new SvgToPdfPage(page, state, options, this._status))
}
/**
@ -1029,15 +1077,13 @@ export class SvgToPdf {
const mode = width > height ? "landscape" : "portrait"
await this.Prepare(language)
console.log("Global prepare done")
this._status.setData("Maps are rendered, building pdf")
console.log("Pages are prepared")
const doc = new jsPDF(mode, undefined, [width, height])
doc.advancedAPI((advancedApi) => {
for (let i = 0; i < this._pages.length; i++) {
console.log("Rendering page", i)
this._status.set("Rendering page "+ i)
if (i > 0) {
const page = this._pages[i]._svgRoot
const width = SvgToPdfInternals.attrNumber(page, "width")
@ -1061,7 +1107,6 @@ export class SvgToPdf {
this._pages[i].drawPage(advancedApi, i, language)
}
})
console.log("Exporting...")
await doc.save(this._title + "." + language + ".pdf")
}

View file

@ -238,7 +238,7 @@
"pdf": {
"attr": "Dades del mapa © Contribuïdors d'OpenStreetMap, reutilitzable sota ODbL",
"attrBackground": "Capa de fons: {background}",
"generatedWith": "Generat amb MapComplete.osm.be",
"generatedWith": "Generat amb MapComplete.osm.be/{layoutid}",
"versionInfo": "v{version} - generat el {date}"
},
"pickLanguage": "Tria idioma: ",

View file

@ -238,7 +238,7 @@
"pdf": {
"attr": "Mapová data © OpenStreetMap Contributors, opakovaně použitelná pod ODbL",
"attrBackground": "Vrstva pozadí: {background}",
"generatedWith": "Generováno pomocí MapComplete.osm.be",
"generatedWith": "Generováno pomocí MapComplete.osm.be/{layoutid}",
"versionInfo": "v{version} - vygenerováno {date}"
},
"pickLanguage": "Vyberte si jazyk: ",

View file

@ -179,7 +179,7 @@
"pdf": {
"attr": "Kortdata © OpenStreetMap Contributors, bearbejdelser under ODbL",
"attrBackground": "Baggrundslag: {background}",
"generatedWith": "Genereret med MapComplete.osm.be",
"generatedWith": "Genereret med MapComplete.osm.be/{layoutid}",
"versionInfo": "v{version} - genereret den {date}"
},
"pickLanguage": "Vælg et sprog: ",

View file

@ -238,7 +238,7 @@
"pdf": {
"attr": "Kartendaten © OpenStreetMap Contributors, wiederverwendbar unter ODbL",
"attrBackground": "Hintergrund: {background}",
"generatedWith": "Erstellt mit MapComplete.osm.be",
"generatedWith": "Erstellt mit MapComplete.osm.be/{layoutid}",
"versionInfo": "v{version} - erstellt am {date}"
},
"pickLanguage": "Sprache auswählen: ",

View file

@ -262,7 +262,7 @@
"pdf": {
"attr": "Map data © OpenStreetMap Contributors, reusable under ODbL",
"attrBackground": "Background layer: {background}",
"generatedWith": "Generated with MapComplete.osm.be",
"generatedWith": "Generated with MapComplete.osm.be/{layoutid}",
"versionInfo": "v{version} - generated on {date}"
},
"pickLanguage": "Choose a language: ",

View file

@ -45,7 +45,7 @@
"pdf": {
"attr": "Mapaj datenoj © Kontribuintoj al OpenStreetMap, reuzeblaj laŭ ODbL",
"attrBackground": "Fona tavolo: {background}",
"generatedWith": "Generita per MapComplete.osm.be",
"generatedWith": "Generita per MapComplete.osm.be/{layoutid}",
"versionInfo": "v{version} - generita je {date}"
},
"pickLanguage": "Elektu lingvon: ",

View file

@ -179,7 +179,7 @@
"pdf": {
"attr": "Datos cartográficos © colaboradores de OpenStreetMap, reutilizables en virtud de la ODbL",
"attrBackground": "Capa de fondo: {background}",
"generatedWith": "Generado como MapComplete.osm.be",
"generatedWith": "Generado como MapComplete.osm.be/{layoutid}",
"versionInfo": "v{version} - generado el {date}"
},
"pickLanguage": "Escoge idioma: ",

View file

@ -202,7 +202,7 @@
"pdf": {
"attr": "Données par © les contributeurs &amp; contributrices OpenStreetMap sous licence libre ODbL",
"attrBackground": "Couche darrière plan : {background}",
"generatedWith": "Généré à laide de MapComplete.osm.be",
"generatedWith": "Généré à laide de MapComplete.osm.be/{layoutid}",
"versionInfo": "v{version} - générée le {date}"
},
"pickLanguage": "Choisir la langue : ",

View file

@ -173,7 +173,7 @@
"pdf": {
"attr": "Térképadatok: © OpenStreetMap-közreműködők; az ODbL licenc szerint újrafelhasználható",
"attrBackground": "Háttérréteg: {background}",
"generatedWith": "Létrehozva a MapComplete.be segítségével",
"generatedWith": "Létrehozva a MapComplete.osm.be/{layoutid} segítségével",
"versionInfo": "{version} verzió létrehozva: {date}"
},
"pickLanguage": "Nyelv kiválasztása: ",

View file

@ -180,7 +180,7 @@
"pdf": {
"attr": "Dati della mappa © OpenStreetMap Contributors, riutilizzabile con licenza ODbL",
"attrBackground": "Livello di sfondo: {background}",
"generatedWith": "Generato con MapComplete.osm.be",
"generatedWith": "Generato con MapComplete.osm.be/{layoutid}",
"versionInfo": "v{version} - generato il {date}"
},
"pickLanguage": "Scegli una lingua: ",

View file

@ -206,7 +206,7 @@
"pdf": {
"attr": "Kartdata © OpenStreetMap-bidragsytere, gjenbrukbart med ODbL-lisens",
"attrBackground": "Bakgrunnslag: {background}",
"generatedWith": "Generert av MapComplete.osm.be",
"generatedWith": "Generert av MapComplete.osm.be/{layoutid}",
"versionInfo": "v{version}. Generert {date}"
},
"pickLanguage": "Velg språk: ",

View file

@ -241,7 +241,7 @@
"pdf": {
"attr": "Kaartgegevens © OpenStreetMap-bijdragers, herbruikbaar volgens ODbL",
"attrBackground": "Achtergrondlaag: {background}",
"generatedWith": "Gemaakt met MapComplete.osm.be",
"generatedWith": "Gemaakt met MapComplete.osm.be/{layoutid}",
"versionInfo": "v{version} - gemaakt op {date}"
},
"pickLanguage": "Kies je taal: ",

View file

@ -218,7 +218,7 @@
"pdf": {
"attr": "Dados do mapa © colaboradores do OpenStreetMap, reutilizáveis sob a licença ODbL",
"attrBackground": "Camada de fundo: {background}",
"generatedWith": "Gerado com o MapComplete.osm.be",
"generatedWith": "Gerado com o MapComplete.osm.be/{layoutid}",
"versionInfo": "v {version} - gerado em {date}"
},
"pickLanguage": "Escolha um idioma: ",

View file

@ -191,7 +191,7 @@
"pdf": {
"attr": "地圖資料 @ 開放街圖貢獻者,採用 ODbL 授權可再利用",
"attrBackground": "背景圖層:{background}",
"generatedWith": "用 MapComplete.osm.be 產生的",
"generatedWith": "用 MapComplete.osm.be/{layoutid} 產生的",
"versionInfo": "v{version} - {date} 產生的"
},
"pickLanguage": "選擇語言: ",

View file

@ -27,6 +27,12 @@
width="646.31287"
height="26.69614"
id="rect10143" />
<rect
x="52.013119"
y="82.676552"
width="85.749054"
height="40.108173"
id="rect13117" />
</defs>
<sodipodi:namedview
id="namedview7"
@ -40,9 +46,9 @@
showgrid="false"
showguides="true"
inkscape:guide-bbox="true"
inkscape:zoom="0.48119622"
inkscape:cx="338.73915"
inkscape:cy="566.29705"
inkscape:zoom="0.44448165"
inkscape:cx="677.19332"
inkscape:cy="1470.2519"
inkscape:window-width="1920"
inkscape:window-height="995"
inkscape:window-x="0"
@ -56,12 +62,12 @@
inkscape:label="bg"
style="display:inline">
<rect
style="fill:none;stroke:#000000;stroke-width:1.34605;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
style="fill:none;stroke:#000000;stroke-width:1.34072;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
id="rect27895"
width="290.34955"
height="403.45847"
x="3.7768779"
y="6.4455185"
width="288.96408"
height="402.18954"
x="3.7742138"
y="6.4428544"
ry="0" />
<rect
style="fill:#ffffff;fill-opacity:0.456196;stroke:#000000;stroke-width:0.581828;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
@ -89,33 +95,33 @@
style="font-style:normal;font-weight:normal;font-size:40px;line-height:0.25;font-family:sans-serif;white-space:pre;shape-inside:url(#rect56707);display:inline;fill:#000000;fill-opacity:1;stroke:#000000;stroke-opacity:1"><tspan
x="0"
y="0"
id="tspan891"><tspan
id="tspan1192"><tspan
style="font-size:13.3333px;-inkscape-font-specification:'sans-serif, Normal'"
id="tspan889">$map(current)</tspan></tspan></text>
id="tspan1190">$map(current)</tspan></tspan></text>
<text
xml:space="preserve"
transform="matrix(0.26458333,0,0,0.26458333,12.08115,27.672609)"
transform="matrix(0.26458333,0,0,0.26458333,45.266489,29.697692)"
id="text3510"
style="font-style:normal;font-weight:normal;font-size:40px;line-height:0;font-family:sans-serif;white-space:pre;shape-inside:url(#rect3512);fill:#000000;fill-opacity:1;stroke:#000000;stroke-opacity:1"><tspan
x="0"
y="0"
id="tspan895"><tspan
id="tspan1196"><tspan
style="font-size:16px;line-height:1.05;-inkscape-font-specification:'sans-serif, Normal'"
id="tspan893">$general.pdf.attr
id="tspan1194">$general.pdf.attr
</tspan></tspan><tspan
x="0"
y="16.799999"
id="tspan899"><tspan
id="tspan1200"><tspan
style="font-size:16px;line-height:1.05;-inkscape-font-specification:'sans-serif, Normal'"
id="tspan897">$general.pdf.attrBackground
id="tspan1198">$general.pdf.attrBackground
</tspan></tspan><tspan
x="0"
y="35.692733"
id="tspan905"><tspan
id="tspan1206"><tspan
style="font-size:16px;line-height:1.05;-inkscape-font-specification:'sans-serif, Normal'"
id="tspan901">$general.pdf.generatedWith</tspan><tspan
id="tspan1202">$general.pdf.generatedWith</tspan><tspan
style="font-size:18.6667px;line-height:1.05;-inkscape-font-specification:'sans-serif, Normal'"
id="tspan903">
id="tspan1204">
</tspan></tspan></text>
<text
xml:space="preserve"
@ -129,9 +135,31 @@
style="font-size:16px;font-family:sans-serif;-inkscape-font-specification:'sans-serif, Normal';text-align:end;white-space:pre;shape-inside:url(#rect10143);fill:#000000;fill-opacity:0.914749;stroke:#ff0000;stroke-width:3.77953;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none"><tspan
x="1182.4844"
y="871.91602"
id="tspan909"><tspan
id="tspan1210"><tspan
style="fill-opacity:1;stroke:none"
id="tspan907">$general.pdf.versionInfo</tspan></tspan></text>
id="tspan1208">$general.pdf.versionInfo</tspan></tspan></text>
<g
id="g1402"
style="display:inline"
transform="translate(-0.22805341,-0.31130177)">
<text
xml:space="preserve"
transform="scale(0.26458333)"
id="text13115"
style="font-size:8px;line-height:1.05;font-family:sans-serif;-inkscape-font-specification:'sans-serif, Normal';white-space:pre;shape-inside:url(#rect13117);fill:#000000;fill-opacity:0.559173;stroke-width:3.77953;stroke-linecap:round;stroke-linejoin:round"><tspan
x="52.013672"
y="88.953906"
id="tspan1212">$img(layouticon)</tspan></text>
<rect
style="fill:#ffffff;fill-opacity:0.559173;stroke:#ff00ff;stroke-width:1;stroke-linecap:round;stroke-linejoin:round;stroke-opacity:1"
id="rect963"
width="29.907761"
height="29.907761"
x="10.527658"
y="12.790291"
rx="0"
ry="0" />
</g>
</g>
<g
inkscape:label="Layer 1"
@ -161,14 +189,14 @@
style="font-style:normal;font-weight:normal;font-size:40px;line-height:1.25;font-family:sans-serif;white-space:pre;shape-inside:url(#rect81706);fill:#000000;fill-opacity:1;stroke:none" />
<text
xml:space="preserve"
transform="matrix(0.26458333,0,0,0.26458333,11.738978,20.267151)"
transform="matrix(0.26458333,0,0,0.26458333,45.026071,20.99111)"
id="text135030"
style="font-style:normal;font-weight:normal;font-size:40px;line-height:1.25;font-family:sans-serif;white-space:pre;shape-inside:url(#rect135032);fill:#000000;fill-opacity:1;stroke:none"><tspan
x="0"
y="0"
id="tspan913"><tspan
id="tspan1216"><tspan
style="font-weight:bold;font-size:34.6667px;-inkscape-font-specification:'sans-serif, Bold'"
id="tspan911">${title}</tspan></tspan></text>
id="tspan1214">${title}</tspan></tspan></text>
<text
xml:space="preserve"
style="font-style:normal;font-weight:normal;font-size:10.5833px;line-height:1.25;font-family:sans-serif;fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.264583"

Before

Width:  |  Height:  |  Size: 8.1 KiB

After

Width:  |  Height:  |  Size: 9.1 KiB

View file

@ -15,11 +15,41 @@
xmlns:svg="http://www.w3.org/2000/svg">
<defs
id="defs33">
<rect
x="52.013117"
y="82.676553"
width="85.749051"
height="40.108173"
id="rect13117" />
<rect
x="44.571302"
y="85.980285"
width="88.070577"
height="21.464239"
id="rect11532" />
<rect
x="146.53725"
y="50.738669"
width="626.74933"
height="43.564572"
id="rect29943" />
<rect
x="143.94905"
y="75.850946"
width="356.14897"
height="104.31819"
id="rect20599" />
<rect
x="146.35217"
y="95.193393"
width="619.52235"
height="67.510544"
id="rect9427" />
<rect
x="39.439771"
y="61.24773"
width="104.91111"
height="99.590532"
width="29.652726"
height="17.960281"
id="rect7785" />
<rect
x="41.547712"
@ -46,9 +76,9 @@
showgrid="false"
showguides="true"
inkscape:guide-bbox="true"
inkscape:zoom="1.3296534"
inkscape:cx="4.5124542"
inkscape:cy="96.641727"
inkscape:zoom="1.5544701"
inkscape:cx="130.59113"
inkscape:cy="85.23805"
inkscape:window-width="1920"
inkscape:window-height="995"
inkscape:window-x="0"
@ -72,7 +102,7 @@
<rect
style="fill:#ffffff;fill-opacity:0.456196;stroke:#000000;stroke-width:0.581828;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
id="rect28206"
width="203.38158"
width="203.382"
height="35.362419"
x="6.3702731"
y="9.6101332"
@ -80,24 +110,13 @@
rx="4.3605742" />
<text
xml:space="preserve"
transform="scale(0.26458333)"
transform="(0.26458333)"
id="text4911"
style="font-style:normal;font-weight:normal;font-size:40px;line-height:1.25;font-family:sans-serif;white-space:pre;shape-inside:url(#rect4913);fill:#000000;fill-opacity:1;stroke:#000000;stroke-opacity:1" />
<text
xml:space="preserve"
transform="scale(0.26458333)"
id="text10253"
style="font-style:normal;font-weight:normal;font-size:40px;line-height:1.25;font-family:sans-serif;white-space:pre;shape-inside:url(#rect10255);fill:#000000;fill-opacity:1;stroke:#000000;stroke-opacity:1" />
<text
xml:space="preserve"
transform="matrix(0.26458333,0,0,0.26458333,14.472331,73.799994)"
id="text56705"
style="font-style:normal;font-weight:normal;font-size:40px;line-height:0.25;font-family:sans-serif;white-space:pre;shape-inside:url(#rect56707);display:inline;fill:#000000;fill-opacity:1;stroke:#000000;stroke-opacity:1"><tspan
x="0"
y="0"
id="tspan2018"><tspan
style="font-size:13.3333px;-inkscape-font-specification:'sans-serif, Normal'"
id="tspan2016">$map(current)</tspan></tspan></text>
<text
xml:space="preserve"
transform="matrix(0.26458333,0,0,0.26458333,38.20272,27.672609)"
@ -105,27 +124,12 @@
style="font-style:normal;font-weight:normal;font-size:40px;line-height:0;font-family:sans-serif;white-space:pre;shape-inside:url(#rect3512);fill:#000000;fill-opacity:1;stroke:#000000;stroke-opacity:1"><tspan
x="0"
y="0"
id="tspan2022"><tspan
style="font-size:16px;line-height:1.05;-inkscape-font-specification:'sans-serif, Normal'"
id="tspan2020">$general.pdf.attr
</tspan></tspan><tspan
x="0"
y="16.799999"
id="tspan2026"><tspan
style="font-size:16px;line-height:1.05;-inkscape-font-specification:'sans-serif, Normal'"
id="tspan2024">$general.pdf.attrBackground
</tspan></tspan><tspan
x="0"
y="35.692733"
id="tspan2032"><tspan
style="font-size:16px;line-height:1.05;-inkscape-font-specification:'sans-serif, Normal'"
id="tspan2028">$general.pdf.generatedWith</tspan><tspan
id="tspan1371"><tspan
style="font-size:18.6667px;line-height:1.05;-inkscape-font-specification:'sans-serif, Normal'"
id="tspan2030">
id="tspan1369">
</tspan></tspan></text>
<text
xml:space="preserve"
transform="scale(0.26458333)"
id="text19136"
style="fill:#000000;-inkscape-font-specification:'sans-serif, Normal';font-family:sans-serif;font-size:16px;text-align:center;stroke-linecap:round;stroke-linejoin:round;stroke-width:0.37795276;stroke:#000000;white-space:pre;shape-inside:url(#rect19138);stroke-opacity:1" />
<text
@ -135,21 +139,57 @@
style="font-size:16px;font-family:sans-serif;-inkscape-font-specification:'sans-serif, Normal';text-align:end;white-space:pre;shape-inside:url(#rect10143);fill:#000000;fill-opacity:0.914749;stroke:#ff0000;stroke-width:3.77953;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none"><tspan
x="1182.4844"
y="871.91602"
id="tspan2036"><tspan
id="tspan1375"><tspan
style="fill-opacity:1;stroke:none"
id="tspan2034">$general.pdf.versionInfo</tspan></tspan></text>
id="tspan1373">$general.pdf.versionInfo</tspan></tspan></text>
<text
xml:space="preserve"
transform="matrix(0.26458333,0,0,0.26458333,-1.5395742,-2.3571711)"
id="text7783"
style="font-size:16px;line-height:1.05;font-family:sans-serif;-inkscape-font-specification:'sans-serif, Normal';text-align:start;white-space:pre;shape-inside:url(#rect7785);fill:#000000;stroke-width:3.77953;stroke-linecap:round;stroke-linejoin:round"
x="-98.273438"
y="0"><tspan
x="39.439453"
y="73.804296"
id="tspan2040"><tspan
style="font-size:8px"
id="tspan2038">$img(layouticon)</tspan></tspan></text>
id="text9425"
style="font-size:8px;line-height:1.05;font-family:sans-serif;-inkscape-font-specification:'sans-serif, Normal';white-space:pre;shape-inside:url(#rect9427);fill:#000000;stroke-width:3.77953;stroke-linecap:round;stroke-linejoin:round"
transform="matrix(0.27887389,0,0,0.27887389,3.4774857,-1.6117409)"><tspan
x="146.35156"
y="109.84234"
id="tspan1379"><tspan
style="font-size:18.6667px"
id="tspan1377">$general.pdf.attr
</tspan></tspan><tspan
x="146.35156"
y="129.44238"
id="tspan1383"><tspan
style="font-size:18.6667px"
id="tspan1381">$general.pdf.attrBackground
</tspan></tspan><tspan
x="146.35156"
y="149.04242"
id="tspan1387"><tspan
style="font-size:18.6667px"
id="tspan1385">$general.pdf.generatedWith</tspan></tspan></text>
<text
xml:space="preserve"
id="text11530"
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:8px;line-height:1.05;font-family:sans-serif;-inkscape-font-specification:'sans-serif, Normal';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-variant-east-asian:normal;white-space:pre;shape-inside:url(#rect11532);fill:#ff0000;fill-opacity:0.559173;stroke:#ff00ff;stroke-width:3.77953;stroke-linecap:round;stroke-linejoin:round"><tspan
x="44.572266"
y="92.258594"
id="tspan1391"><tspan
style="fill:#000000;stroke:none"
id="tspan1389">$map(current)</tspan></tspan></text>
<text
xml:space="preserve"
transform="scale(0.26458333)"
id="text13115"
style="font-size:8px;line-height:1.05;font-family:sans-serif;-inkscape-font-specification:'sans-serif, Normal';white-space:pre;shape-inside:url(#rect13117);fill:#000000;fill-opacity:0.559173;stroke-width:3.77953;stroke-linecap:round;stroke-linejoin:round"><tspan
x="52.013672"
y="88.953906"
id="tspan1393">$img(layouticon)</tspan></text>
<rect
style="fill:#ffffff;fill-opacity:0.55917299;stroke-width:1;stroke-linecap:round;stroke-linejoin:round;stroke:#ff00ff;stroke-opacity:1"
id="rect963"
width="29.907761"
height="29.907761"
x="10.527658"
y="12.790291"
rx="0"
ry="0" />
</g>
<g
inkscape:label="Layer 1"
@ -158,7 +198,6 @@
style="display:inline">
<text
xml:space="preserve"
transform="scale(0.26458333)"
id="text62796"
style="font-style:normal;font-weight:normal;font-size:40px;line-height:1.25;font-family:sans-serif;white-space:pre;shape-inside:url(#rect62798);fill:#000000;fill-opacity:1;stroke:none" />
<text
@ -174,19 +213,8 @@
id="tspan8613-8" /></text>
<text
xml:space="preserve"
transform="scale(0.26458333)"
id="text81704"
style="font-style:normal;font-weight:normal;font-size:40px;line-height:1.25;font-family:sans-serif;white-space:pre;shape-inside:url(#rect81706);fill:#000000;fill-opacity:1;stroke:none" />
<text
xml:space="preserve"
transform="matrix(0.26458333,0,0,0.26458333,37.860548,20.267151)"
id="text135030"
style="font-style:normal;font-weight:normal;font-size:40px;line-height:1.25;font-family:sans-serif;white-space:pre;shape-inside:url(#rect135032);fill:#000000;fill-opacity:1;stroke:none"><tspan
x="0"
y="0"
id="tspan2044"><tspan
style="font-weight:bold;font-size:34.6667px;-inkscape-font-specification:'sans-serif, Bold'"
id="tspan2042">${title}</tspan></tspan></text>
<text
xml:space="preserve"
style="font-style:normal;font-weight:normal;font-size:10.5833px;line-height:1.25;font-family:sans-serif;fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.264583"
@ -203,5 +231,19 @@
id="tspan53311"
x="105.86118"
y="129.4847" /></text>
<text
xml:space="preserve"
id="text20597"
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:18.6667px;line-height:1.05;font-family:sans-serif;-inkscape-font-specification:'sans-serif, Normal';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-variant-east-asian:normal;white-space:pre;shape-inside:url(#rect20599);fill:#000000;stroke-width:3.77953;stroke-linecap:round;stroke-linejoin:round" />
<text
xml:space="preserve"
id="text29941"
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:18.6667px;line-height:1.05;font-family:sans-serif;-inkscape-font-specification:'sans-serif, Normal';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-variant-east-asian:normal;white-space:pre;shape-inside:url(#rect29943);fill:#000000;stroke-width:3.77953;stroke-linecap:round;stroke-linejoin:round"
transform="matrix(0.27887389,0,0,0.27887389,3.2770649,-0.61374399)"><tspan
x="146.53711"
y="77.943514"
id="tspan1397"><tspan
style="font-weight:bold;font-size:34.6667px;-inkscape-font-specification:'sans-serif, Bold'"
id="tspan1395">${title}</tspan></tspan></text>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 8.8 KiB

After

Width:  |  Height:  |  Size: 11 KiB