forked from MapComplete/MapComplete
Fix: improve PDF-output functionality
This commit is contained in:
parent
c6283ac720
commit
215286a5af
22 changed files with 363 additions and 221 deletions
|
@ -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}/>
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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
|
||||
|
||||
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",
|
||||
generateImage(key: string): HTMLImageElement {
|
||||
downloadHelper.generateImage(key)
|
||||
},
|
||||
textSubstitutions: <Record<string, string>> {
|
||||
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),
|
||||
date: new Date().toISOString().substring(0, 16),
|
||||
background: new Translation(bg.properties.name).txt
|
||||
}
|
||||
})
|
||||
|
||||
const unsub = creator.status.addCallbackAndRunD(s => status?.setData(s))
|
||||
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}
|
||||
/>
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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(/ /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]
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -689,12 +720,13 @@ class SvgToPdfPage {
|
|||
this.options.beforePage(i)
|
||||
}
|
||||
const self = this
|
||||
const internal = new SvgToPdfInternals(advancedApi, this.images, this.rects, (key) =>
|
||||
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")
|
||||
}
|
||||
|
||||
|
|
|
@ -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: ",
|
||||
|
|
|
@ -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: ",
|
||||
|
|
|
@ -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: ",
|
||||
|
|
|
@ -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: ",
|
||||
|
|
|
@ -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: ",
|
||||
|
|
|
@ -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: ",
|
||||
|
|
|
@ -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: ",
|
||||
|
|
|
@ -202,7 +202,7 @@
|
|||
"pdf": {
|
||||
"attr": "Données par © les contributeurs & contributrices OpenStreetMap sous licence libre ODbL",
|
||||
"attrBackground": "Couche d’arrière plan : {background}",
|
||||
"generatedWith": "Généré à l’aide de MapComplete.osm.be",
|
||||
"generatedWith": "Généré à l’aide de MapComplete.osm.be/{layoutid}",
|
||||
"versionInfo": "v{version} - générée le {date}"
|
||||
},
|
||||
"pickLanguage": "Choisir la langue : ",
|
||||
|
|
|
@ -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: ",
|
||||
|
|
|
@ -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: ",
|
||||
|
|
|
@ -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: ",
|
||||
|
|
|
@ -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: ",
|
||||
|
|
|
@ -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: ",
|
||||
|
|
|
@ -191,7 +191,7 @@
|
|||
"pdf": {
|
||||
"attr": "地圖資料 @ 開放街圖貢獻者,採用 ODbL 授權可再利用",
|
||||
"attrBackground": "背景圖層:{background}",
|
||||
"generatedWith": "用 MapComplete.osm.be 產生的",
|
||||
"generatedWith": "用 MapComplete.osm.be/{layoutid} 產生的",
|
||||
"versionInfo": "v{version} - {date} 產生的"
|
||||
},
|
||||
"pickLanguage": "選擇語言: ",
|
||||
|
|
|
@ -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 |
|
@ -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 |
Loading…
Reference in a new issue