Fix: decent PDF-export

This commit is contained in:
Pieter Vander Vennet 2023-06-04 22:52:13 +02:00
parent 905f796baa
commit de20b00b8f
15 changed files with 619 additions and 1396 deletions

View file

@ -84,7 +84,7 @@ export default class TileLocalStorage<T> {
const maxAge = this._maxAgeSeconds
const timeDiff = Date.now() - date
if (timeDiff >= maxAge) {
console.log("Dropping cache for", this._layername, tileIndex, "out of date")
console.debug("Dropping cache for", this._layername, tileIndex, "out of date")
await IdbLocalStorage.SetDirectly(this._layername + "_" + tileIndex, undefined)
return undefined

View file

@ -65,14 +65,14 @@ export default class GeoJsonSource implements FeatureSource {
return
}
this.LoadJSONFrom(url, eventsource, layer)
.then((fs) => console.log("Loaded",fs.length, "features from", url))
.catch((err) => console.error("Could not load ", url, "due to", err))
.then((fs) => console.debug("Loaded",fs.length, "features from", url))
.catch((err) => console.warn("Could not load ", url, "due to", err))
return true // data is loaded, we can safely unregister
})
} else {
this.LoadJSONFrom(url, eventsource, layer)
.then((fs) => console.log("Loaded",fs.length, "features from", url))
.catch((err) => console.error("Could not load ", url, "due to", err))
.then((fs) => console.debug("Loaded",fs.length, "features from", url))
.catch((err) => console.warn("Could not load ", url, "due to", err))
}
this.features = eventsource
}

View file

@ -41,7 +41,7 @@ export class AvailableRasterLayers {
type: "Feature",
properties: {
name: "MapTiler",
url: null,
url: "https://api.maptiler.com/maps/15cc8f61-0353-4be6-b8da-13daea5f7432/style.json?key=GvoVAJgu46I5rZapJuAy",
category: "osmbasedmap",
id: "maptiler",
attribution: {

View file

@ -159,7 +159,7 @@ export default class ThemeViewState implements SpecialVisualizationState {
*/
if(this.layout.layers.some(l => l._needsFullNodeDatabase)){
if (this.layout.layers.some(l => l._needsFullNodeDatabase)) {
this.fullNodeDatabase = new FullNodeDatabaseSource()
}
@ -181,9 +181,10 @@ export default class ThemeViewState implements SpecialVisualizationState {
}
currentViewIndex++
return <Feature[]>[bbox.asGeoJson({
zoom: this.mapProperties.zoom.data,
...this.mapProperties.location.data,
id: "current_view" }
zoom: this.mapProperties.zoom.data,
...this.mapProperties.location.data,
id: "current_view"
}
)];
}
)
@ -240,39 +241,6 @@ export default class ThemeViewState implements SpecialVisualizationState {
this.featureProperties,
fs.layer.layerDef.maxAgeOfCache
)
const doShowLayer = this.mapProperties.zoom.map(
(z) =>
(fs.layer.isDisplayed?.data ?? true) && z >= (fs.layer.layerDef?.minzoom ?? 0),
[fs.layer.isDisplayed]
)
if (
!doShowLayer.data &&
(this.featureSwitches.featureSwitchFilter.data === false || !fs.layer.layerDef.name)
) {
/* This layer is hidden and there is no way to enable it (filterview is disabled or this layer doesn't show up in the filter view as the name is not defined)
*
* This means that we don't have to filter it, nor do we have to display it
* */
return
}
const filtered = new FilteringFeatureSource(
fs.layer,
fs,
(id) => this.featureProperties.getStore(id),
this.layerState.globalFilters
)
new ShowDataLayer(this.map, {
layer: fs.layer.layerDef,
features: filtered,
doShowLayer,
selectedElement: this.selectedElement,
selectedLayer: this.selectedLayer,
fetchStore: (id) => this.featureProperties.getStore(id),
})
})
this.floors = this.featuresInView.features.stabilized(500).map((features) => {
@ -317,6 +285,7 @@ export default class ThemeViewState implements SpecialVisualizationState {
this.changes
)
this.showNormalDataOn(this.map)
this.initActors()
this.addLastClick(lastClick)
this.drawSpecialLayers()
@ -327,6 +296,42 @@ export default class ThemeViewState implements SpecialVisualizationState {
}
}
public showNormalDataOn(map: Store<MlMap>) {
this.perLayer.forEach((fs) => {
const doShowLayer = this.mapProperties.zoom.map(
(z) =>
(fs.layer.isDisplayed?.data ?? true) && z >= (fs.layer.layerDef?.minzoom ?? 0),
[fs.layer.isDisplayed]
)
if (
!doShowLayer.data &&
(this.featureSwitches.featureSwitchFilter.data === false || !fs.layer.layerDef.name)
) {
/* This layer is hidden and there is no way to enable it (filterview is disabled or this layer doesn't show up in the filter view as the name is not defined)
*
* This means that we don't have to filter it, nor do we have to display it
* */
return
}
const filtered = new FilteringFeatureSource(
fs.layer,
fs,
(id) => this.featureProperties.getStore(id),
this.layerState.globalFilters
)
new ShowDataLayer(map, {
layer: fs.layer.layerDef,
features: filtered,
doShowLayer,
selectedElement: this.selectedElement,
selectedLayer: this.selectedLayer,
fetchStore: (id) => this.featureProperties.getStore(id),
})
})
}
/**
* Various small methods that need to be called
*/

View file

@ -1,378 +0,0 @@
import Combine from "../Base/Combine"
import {FlowPanelFactory, FlowStep} from "../ImportFlow/FlowStep"
import {ImmutableStore, Store, UIEventSource} from "../../Logic/UIEventSource"
import {InputElement} from "../Input/InputElement"
import {SvgToPdf, SvgToPdfOptions} from "../../Utils/svgToPdf"
import {FixedInputElement} from "../Input/FixedInputElement"
import {FixedUiElement} from "../Base/FixedUiElement"
import FileSelectorButton from "../Input/FileSelectorButton"
import InputElementMap from "../Input/InputElementMap"
import {RadioButton} from "../Input/RadioButton"
import {Utils} from "../../Utils"
import {VariableUiElement} from "../Base/VariableUIElement"
import Loading from "../Base/Loading"
import BaseUIElement from "../BaseUIElement"
import Img from "../Base/Img"
import Title from "../Base/Title"
import {CheckBox} from "../Input/Checkboxes"
import Toggle from "../Input/Toggle"
import List from "../Base/List"
import LeftIndex from "../Base/LeftIndex"
import Constants from "../../Models/Constants"
import Toggleable from "../Base/Toggleable"
import Lazy from "../Base/Lazy"
import LinkToWeblate from "../Base/LinkToWeblate"
import Link from "../Base/Link"
import {AllLanguagesSelector} from "../Popup/AllLanguagesSelector"
class SelectTemplate extends Combine implements FlowStep<{ title: string; pages: string[] }> {
readonly IsValid: Store<boolean>
readonly Value: Store<{ title: string; pages: string[] }>
constructor() {
const elements: InputElement<{ templateName: string; pages: string[] }>[] = []
for (const templateName in SvgToPdf.templates) {
const template = SvgToPdf.templates[templateName]
elements.push(
new FixedInputElement(
new Combine([
new FixedUiElement(templateName).SetClass("font-bold pr-2"),
template.description,
]),
new UIEventSource({ templateName, pages: template.pages })
)
)
}
const file = new FileSelectorButton(
new FixedUiElement("Select an svg image which acts as template"),
{
acceptType: "image/svg+xml",
allowMultiple: true,
}
)
const fileMapped = new InputElementMap<
FileList,
{ templateName: string; pages: string[]; fromFile: true }
>(
file,
(x0, x1) => x0 === x1,
(filelist) => {
if (filelist === undefined) {
return undefined
}
const pages = []
let templateName: string = undefined
for (const file of Array.from(filelist)) {
if (templateName == undefined) {
templateName = file.name.substring(file.name.lastIndexOf("/") + 1)
templateName = templateName.substring(0, templateName.lastIndexOf("."))
}
pages.push(file.text())
}
return {
templateName,
pages,
fromFile: true,
}
},
(_) => undefined
)
elements.push(fileMapped)
const radio = new RadioButton(elements, { selectFirstAsDefault: true })
const loaded: Store<{ success: { title: string; pages: string[] } } | { error: any }> =
radio.GetValue().bind((template) => {
if (template === undefined) {
return undefined
}
if (template["fromFile"]) {
return UIEventSource.FromPromiseWithErr(
Promise.all(template.pages).then((pages) => ({
title: template.templateName,
pages,
}))
)
}
const urls = template.pages.map((p) => SelectTemplate.ToUrl(p))
const dloadAll: Promise<{ title: string; pages: string[] }> = Promise.all(
urls.map((url) => Utils.download(url))
).then((pages) => ({
pages,
title: template.templateName,
}))
return UIEventSource.FromPromiseWithErr(dloadAll)
})
const preview = new VariableUiElement(
loaded.map((pages) => {
if (pages === undefined) {
return new Loading()
}
if (pages["error"] !== undefined) {
return new FixedUiElement("Loading preview failed: " + pages["err"]).SetClass(
"alert"
)
}
const svgs = pages["success"].pages
if (svgs.length === 0) {
return new FixedUiElement("No pages loaded...").SetClass("alert")
}
const els: BaseUIElement[] = []
for (const pageSrc of svgs) {
const el = new Img(pageSrc, true).SetClass("w-96 m-2 border-black border-2")
els.push(el)
}
return new Combine(els).SetClass("flex border border-subtle rounded-xl")
})
)
super([new Title("Select template"), radio, new Title("Preview"), preview])
this.Value = loaded.map((l) => (l === undefined ? undefined : l["success"]))
this.IsValid = this.Value.map((v) => v !== undefined)
}
public static ToUrl(spec: string) {
if (spec.startsWith("http")) {
return spec
}
let path = window.location.pathname
path = path.substring(0, path.lastIndexOf("/"))
return window.location.protocol + "//" + window.location.host + path + "/" + spec
}
}
class SelectPdfOptions
extends Combine
implements FlowStep<{ title: string; pages: string[]; options: SvgToPdfOptions }>
{
readonly IsValid: Store<boolean>
readonly Value: Store<{ title: string; pages: string[]; options: SvgToPdfOptions }>
constructor(title: string, pages: string[], getFreeDiv: () => string) {
const dummy = new CheckBox("Don't add data to the map (to quickly preview the PDF)", false)
const overrideMapLocation = new CheckBox(
"Override map location: use a selected location instead of the location set in the template",
false
)
const locationInput = Minimap.createMiniMap().SetClass("block w-full")
const searchField = undefined // new SearchAndGo({ leafletMap: locationInput.leafletMap })
const selectLocation = new Combine([
new Toggle(
new Combine([new Title("Select override location"), searchField]).SetClass("flex"),
undefined,
overrideMapLocation.GetValue()
),
new Toggle(
locationInput.SetStyle("height: 20rem"),
undefined,
overrideMapLocation.GetValue()
).SetStyle("height: 20rem"),
])
.SetClass("block")
.SetStyle("height: 25rem")
super([new Title("Select options"), dummy, overrideMapLocation, selectLocation])
this.Value = dummy.GetValue().map(
(disableMaps) => {
return {
pages,
title,
options: <SvgToPdfOptions>{
disableMaps,
getFreeDiv,
overrideLocation: overrideMapLocation.GetValue().data
? locationInput.location.data
: undefined,
},
}
},
[overrideMapLocation.GetValue(), locationInput.location]
)
this.IsValid = new ImmutableStore(true)
}
}
class PreparePdf extends Combine implements FlowStep<{ svgToPdf: SvgToPdf; languages: string[] }> {
readonly IsValid: Store<boolean>
readonly Value: Store<{ svgToPdf: SvgToPdf; languages: string[] }>
constructor(title: string, pages: string[], options: SvgToPdfOptions) {
const svgToPdf = new SvgToPdf(title, pages, options)
const languageSelector = new AllLanguagesSelector()
const isPrepared = UIEventSource.FromPromiseWithErr(svgToPdf.Prepare())
super([
new Title("Select languages..."),
languageSelector,
new Toggle(
new Loading("Preparing maps..."),
undefined,
isPrepared.map((p) => p === undefined)
),
])
this.Value = isPrepared.map(
(isPrepped) => {
if (isPrepped === undefined) {
return undefined
}
if (isPrepped["success"] !== undefined) {
const svgToPdf = isPrepped["success"]
const langs = languageSelector.GetValue().data
console.log("Languages are", langs)
if (langs.length === 0) {
return undefined
}
return { svgToPdf, languages: langs }
}
return undefined
},
[languageSelector.GetValue()]
)
this.IsValid = this.Value.map((v) => v !== undefined)
}
}
class InspectStrings
extends Toggle
implements FlowStep<{ svgToPdf: SvgToPdf; languages: string[] }>
{
readonly IsValid: Store<boolean>
readonly Value: Store<{ svgToPdf: SvgToPdf; languages: string[] }>
constructor(svgToPdf: SvgToPdf, languages: string[]) {
const didLoadLanguages = UIEventSource.FromPromiseWithErr(
svgToPdf.PrepareLanguages(languages)
).map((l) => l !== undefined && l["success"] !== undefined)
super(
new Combine([
new Title("Inspect translation strings"),
...languages.map(
(l) => new Lazy(() => InspectStrings.createOverviewPanel(svgToPdf, l))
),
]),
new Loading(),
didLoadLanguages
)
this.Value = new ImmutableStore({ svgToPdf, languages })
this.IsValid = didLoadLanguages
}
private static createOverviewPanel(svgToPdf: SvgToPdf, language: string): BaseUIElement {
const elements: BaseUIElement[] = []
let foundTranslations = 0
const allKeys = Array.from(svgToPdf.translationKeys())
for (const translationKey of allKeys) {
let spec = translationKey
if (translationKey.startsWith("layer.")) {
spec = "layers:" + translationKey.substring(6)
} else {
spec = "core:" + translationKey
}
const translated = svgToPdf.getTranslation("$" + translationKey, language, true)
if (translated) {
foundTranslations++
}
const linkToWeblate = new Link(
spec,
LinkToWeblate.hrefToWeblate(language, spec),
true
).SetClass("font-bold link-underline")
elements.push(
new Combine([
linkToWeblate,
"&nbsp;",
translated ?? new FixedUiElement("No translation found!").SetClass("alert"),
])
)
}
return new Toggleable(
new Title("Translations for " + language),
new Combine([
`${foundTranslations}/${allKeys.length} of translations are found (${Math.floor(
(100 * foundTranslations) / allKeys.length
)}%)`,
"The following keys are used:",
new List(elements),
]),
{ closeOnClick: false, height: "15rem" }
)
}
}
class SavePdf extends Combine {
constructor(svgToPdf: SvgToPdf, languages: string[]) {
super([
new Title("Generating your pdfs..."),
new List(
languages.map(
(lng) =>
new Toggle(
lng + " is done!",
new Loading("Creating pdf for " + lng),
UIEventSource.FromPromiseWithErr(
svgToPdf.ConvertSvg(lng).then(() => true)
).map((x) => x !== undefined && x["success"] === true)
)
)
),
])
}
}
export class PdfExportGui extends LeftIndex {
constructor(freeDivId: string) {
let i = 0
const createDiv = (): string => {
const div = document.createElement("div")
div.id = "freediv-" + i++
document.getElementById(freeDivId).append(div)
return div.id
}
Constants.defaultOverpassUrls.splice(0, 1)
const { flow, furthestStep, titles } = FlowPanelFactory.start(
new Title("Select template"),
new SelectTemplate()
)
.then(
new Title("Select options"),
({ title, pages }) => new SelectPdfOptions(title, pages, createDiv)
)
.then(
"Generate maps...",
({ title, pages, options }) => new PreparePdf(title, pages, options)
)
.then(
"Inspect translations",
({ svgToPdf, languages }) => new InspectStrings(svgToPdf, languages)
)
.finish("Generating...", ({ svgToPdf, languages }) => new SavePdf(svgToPdf, languages))
const toc = new List(
titles.map(
(title, i) =>
new VariableUiElement(
furthestStep.map((currentStep) => {
if (i > currentStep) {
return new Combine([title]).SetClass("subtle")
}
if (i == currentStep) {
return new Combine([title]).SetClass("font-bold")
}
if (i < currentStep) {
return title
}
})
)
),
true
)
const leftContents: BaseUIElement[] = [toc].map((el) => el?.SetClass("pl-4"))
super(leftContents, flow)
}
}

View file

@ -10,12 +10,13 @@
import DownloadHelper from "./DownloadHelper";
import {Utils} from "../../Utils";
import type {PriviligedLayerType} from "../../Models/Constants";
import {UIEventSource} from "../../Logic/UIEventSource";
export let state: SpecialVisualizationState
export let extension: string
export let mimetype: string
export let construct: (geojsonCleaned: FeatureCollection) => (Blob | string) | Promise<void>
export let construct: (geojsonCleaned: FeatureCollection, title: string) => (Blob | string) | Promise<void>
export let mainText: Translation
export let helperText: Translation
export let metaIsIncluded: boolean
@ -26,51 +27,63 @@
let isExporting = false
let isError = false
let status: UIEventSource<string> = new UIEventSource<string>(undefined)
async function clicked() {
isExporting = true
const gpsLayer = state.layerState.filteredLayers.get(
<PriviligedLayerType>"gps_location"
)
state.lastClickObject.features.setData([])
const gpsIsDisplayed = gpsLayer.isDisplayed.data
try {
gpsLayer.isDisplayed.setData(false)
const geojson: FeatureCollection = downloadHelper.getCleanGeoJson(metaIsIncluded)
const name = state.layout.id
const promise = construct(geojson)
const title = `MapComplete_${name}_export_${new Date().toISOString().substr(0, 19)}.${extension}`
const promise = construct(geojson, title)
let data: Blob | string
if (typeof promise === "string") {
data = promise
} else if (typeof promise["then"] === "function") {
data = await <Promise<Blob | string>> promise
data = await <Promise<Blob | string>>promise
} else {
data = <Blob>promise
}
if (!data) {
return
}
console.log("Got data", data)
Utils.offerContentsAsDownloadableFile(
data,
`MapComplete_${name}_export_${new Date().toISOString().substr(0, 19)}.${extension}`,
title,
{
mimetype,
}
)
} catch (e) {
isError = true
console.error(e)
} finally {
isExporting = false
gpsLayer.isDisplayed.setData(gpsIsDisplayed)
}
gpsLayer.isDisplayed.setData(gpsIsDisplayed)
isExporting = false
}
</script>
{#if isError}
<Tr cls="alert" t={Translations.t.error}/>
<Tr cls="alert" t={Translations.t.general.error}/>
{:else if isExporting}
<Loading>
<Tr t={t.exporting}/>
{#if $status}
{$status}
{:else}
<Tr t={t.exporting}/>
{/if}
</Loading>
{:else}
<button class="flex w-full" on:click={clicked}>

View file

@ -1,14 +1,21 @@
<script lang="ts">
import type {SpecialVisualizationState} from "../SpecialVisualization";
import Loading from "../Base/Loading.svelte";
import Translations from "../i18n/Translations";
import Tr from "../Base/Tr.svelte";
import DownloadHelper from "./DownloadHelper";
import DownloadButton from "./DownloadButton.svelte";
import {GeoOperations} from "../../Logic/GeoOperations";
import {SvgToPdf} from "../../Utils/svgToPdf";
import Locale from "../i18n/Locale";
import ThemeViewState from "../../Models/ThemeViewState";
import {Utils} from "../../Utils";
import Constants from "../../Models/Constants";
import {Translation} from "../i18n/Translation";
import {AvailableRasterLayers} from "../../Models/RasterLayers";
import {UIEventSource} from "../../Logic/UIEventSource";
export let state: SpecialVisualizationState
export let state: ThemeViewState
let isLoading = state.dataIsLoading
const t = Translations.t.general.download
@ -30,6 +37,30 @@
})
}
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",
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
}
</script>
@ -85,7 +116,7 @@
extension="pdf"
mainText={t.downloadAsPdf}
helperText={t.downloadAsPdfHelper}
construct={_ => state.mapProperties.exportAsPng(4)}
construct={constructPdf}
/>

View file

@ -199,7 +199,7 @@ export class MapLibreAdaptor implements MapProperties, ExportableMap {
return url
}
private static setDpi(drawOn: HTMLCanvasElement, ctx: CanvasRenderingContext2D, dpiFactor: number) {
public static setDpi(drawOn: HTMLCanvasElement, ctx: CanvasRenderingContext2D, dpiFactor: number) {
drawOn.style.width = drawOn.style.width || drawOn.width + "px"
drawOn.style.height = drawOn.style.height || drawOn.height + "px"
@ -223,23 +223,16 @@ export class MapLibreAdaptor implements MapProperties, ExportableMap {
console.log("Canvas size:", drawOn.width, drawOn.height)
const ctx = drawOn.getContext("2d")
// Set up CSS size.
MapLibreAdaptor.setDpi(drawOn, ctx, dpiFactor)
MapLibreAdaptor.setDpi(drawOn, ctx, dpiFactor / map.getPixelRatio())
await this.exportBackgroundOnCanvas(drawOn, ctx, dpiFactor)
await this.exportBackgroundOnCanvas(ctx)
drawOn.toBlob(blob => {
Utils.offerContentsAsDownloadableFile(blob, "bg.png")
})
console.log("Getting markers")
// MapLibreAdaptor.setDpi(drawOn, ctx, 1)
const markers = await this.drawMarkers(dpiFactor)
console.log("Drawing markers (" + markers.width + "*" + markers.height + ") onto drawOn (" + drawOn.width + "*" + drawOn.height + ")")
ctx.scale(1/dpiFactor,1/dpiFactor )
ctx.drawImage(markers, 0, 0, drawOn.width, drawOn.height)
ctx.scale(dpiFactor, dpiFactor)
markers.toBlob(blob => {
Utils.offerContentsAsDownloadableFile(blob, "markers.json")
})
this._maplibreMap.data?.resize()
return await new Promise<Blob>(resolve => drawOn.toBlob(blob => resolve(blob)))
}
@ -248,7 +241,7 @@ export class MapLibreAdaptor implements MapProperties, ExportableMap {
* Exports the background map and lines to PNG.
* Markers are _not_ rendered
*/
private async exportBackgroundOnCanvas(drawOn: HTMLCanvasElement, ctx: CanvasRenderingContext2D, dpiFactor: number = 1): Promise<void> {
private async exportBackgroundOnCanvas(ctx: CanvasRenderingContext2D): Promise<void> {
const map = this._maplibreMap.data
// We draw the maplibre-map onto the canvas. This does not export markers
// Inspiration by https://github.com/mapbox/mapbox-gl-js/issues/2766

View file

@ -8,6 +8,7 @@
import { Map } from "@onsvisual/svelte-maps";
import type { Map as MaplibreMap } from "maplibre-gl";
import type { Writable } from "svelte/store";
import {AvailableRasterLayers} from "../../Models/RasterLayers";
/**
@ -24,7 +25,7 @@
$map.resize();
});
});
const styleUrl = "https://api.maptiler.com/maps/15cc8f61-0353-4be6-b8da-13daea5f7432/style.json?key=GvoVAJgu46I5rZapJuAy";
const styleUrl = AvailableRasterLayers.maplibre.properties.url;
</script>
<main>
<Map bind:center={center}

View file

@ -1,8 +1,9 @@
import ThemeViewState from "../Models/ThemeViewState"
import SvelteUIElement from "../UI/Base/SvelteUIElement"
import MaplibreMap from "../UI/Map/MaplibreMap.svelte"
import { Utils } from "../Utils"
import { UIEventSource } from "../Logic/UIEventSource"
import {Utils} from "../Utils"
import {UIEventSource} from "../Logic/UIEventSource"
import {Map as MlMap} from "maplibre-gl"
import {MapLibreAdaptor} from "../UI/Map/MapLibreAdaptor";
import {AvailableRasterLayers} from "../Models/RasterLayers";
export interface PngMapCreatorOptions {
readonly width: number
@ -23,33 +24,56 @@ export class PngMapCreator {
* Creates a base64-encoded PNG image
* @constructor
*/
public async CreatePng(status: UIEventSource<string>): Promise<Blob> {
public async CreatePng(freeComponentId: string, status?: UIEventSource<string>): Promise<Blob> {
const div = document.createElement("div")
div.id = "mapdiv-" + PngMapCreator.id
div.style.width = this._options.width + "mm"
div.style.height = this._options.height + "mm"
PngMapCreator.id++
const layout = this._state.layout
function setState(msg: string) {
status.setData(layout.id + ": " + msg)
status?.setData(layout.id + ": " + msg)
}
setState("Initializing map")
const map = this._state.map
new SvelteUIElement(MaplibreMap, { map })
.SetStyle(
"width: " + this._options.width + "mm; height: " + this._options.height + "mm; border: 2px solid red;"
)
.AttachTo("extradiv")
map.data.resize()
const settings = this._state.mapProperties
const l = settings.location.data
document.getElementById(freeComponentId).appendChild(div)
const mapElem = new MlMap({
container: div.id,
style: AvailableRasterLayers.maplibre.properties.url,
center: [l.lon, l.lat],
zoom: settings.zoom.data,
pixelRatio: 6
});
const map = new UIEventSource<MlMap>(mapElem)
const mla = new MapLibreAdaptor(map)
mla.zoom.setData(settings.zoom.data)
mla.location.setData(settings.location.data)
mla.rasterLayer.setData(settings.rasterLayer.data)
this._state?.showNormalDataOn(map)
console.log("Creating a map with size", this._options.width, this._options.height)
setState("Waiting for the data")
await this._state.dataIsLoading.AsPromise((loading) => !loading)
setState("Waiting for styles to be fully loaded")
while (!map?.data?.isStyleLoaded()) {
console.log("Waiting for the style to be loaded...")
await Utils.waitFor(250)
}
// Some extra buffer...
await Utils.waitFor(1000)
setState("Exporting png")
console.log("Loading for", this._state.layout.id, "is done")
console.log("Map export: starting actual export, target size is", this._options.width,"mm * ",this._options.height+"mm")
return this._state.mapProperties.exportAsPng(4)
const png = await mla.exportAsPng(6)
div.parentElement.removeChild(div)
Utils.offerContentsAsDownloadableFile(png, "test.png")
return png
}
}

View file

@ -12,7 +12,6 @@ import Constants from "../Models/Constants"
import Hash from "../Logic/Web/Hash"
import ThemeViewState from "../Models/ThemeViewState"
import {Store, UIEventSource} from "../Logic/UIEventSource"
import {FixedUiElement} from "../UI/Base/FixedUiElement"
class SvgToPdfInternals {
private static readonly dummyDoc: jsPDF = new jsPDF()
@ -260,15 +259,29 @@ class SvgToPdfInternals {
const ry = SvgToPdfInternals.attrNumber(element, "ry", false) ?? 0
const rx = SvgToPdfInternals.attrNumber(element, "rx", false) ?? 0
const css = SvgToPdfInternals.css(element)
this.doc.saveGraphicsState()
if (css["fill-opacity"] !== "0" && css["fill"] !== "none") {
this.doc.setFillColor(css["fill"] ?? "black")
let color = css["fill"] ?? "black"
let opacity = 1
if (css["fill-opacity"]) {
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")
}
if (css["stroke"] && css["stroke"] !== "none") {
this.doc.setLineWidth(Number(css["stroke-width"] ?? 1))
this.doc.setDrawColor(css["stroke"] ?? "black")
if (css["opacity"]) {
const opacity = Number(css["opacity"])
this.doc.setGState(this.doc.GState({"stroke-opacity": opacity}))
}
this.doc.roundedRect(x, y, width, height, rx, ry, "S")
}
this.doc.restoreGraphicsState()
return
}
@ -290,13 +303,27 @@ class SvgToPdfInternals {
}
private drawTspan(tspan: Element) {
if (tspan.textContent == "") {
const txt = tspan.textContent
if (txt == "") {
return
}
const x = SvgToPdfInternals.attrNumber(tspan, "x")
const y = SvgToPdfInternals.attrNumber(tspan, "y")
const css = SvgToPdfInternals.css(tspan)
const imageMatch = txt.match(/\$img\(([^)])+\)/)
if (imageMatch) {
const [key, width, height] = imageMatch[1].split(",").map(s => s.trim())
const url = key.startsWith("http") ? key : this.extractTranslation("${" + key + "}")
const img = this._images[url]
console.log("Drawing an injected image", {key, url, img: img.src})
this.doc.addImage(
img.src
, x, y, Number(width), Number(height)
)
return
}
let maxWidth: number = undefined
if (css["shape-inside"]) {
const matched = css["shape-inside"].match(/url\(#([a-zA-Z0-9-]+)\)/)
@ -322,7 +349,6 @@ class SvgToPdfInternals {
this.doc.setTextColor("black")
}
let fontsize = parseFloat(css["font-size"])
this.doc.setFontSize(fontsize * 2.5)
let textTemplate = tspan.textContent.split(" ")
@ -339,7 +365,13 @@ class SvgToPdfInternals {
addSpace = false
continue
}
if (text.startsWith(`$\{`)) {
if (addSpace) {
result += " "
}
result += this.extractTranslation(text)
continue
}
if (!text.startsWith("$")) {
if (addSpace) {
result += " "
@ -496,11 +528,16 @@ class SvgToPdfInternals {
}
export interface SvgToPdfOptions {
freeComponentId: string,
disableMaps?: false | true
textSubstitutions?: Record<string, string>
beforePage?: (i: number) => void
overrideLocation?: { lat: number; lon: number },
disableDataLoading?: boolean | false
disableDataLoading?: boolean | false,
/**
* Override all the maps to generate with this map
*/
state?: ThemeViewState
}
class SvgToPdfPage {
@ -569,7 +606,6 @@ class SvgToPdfPage {
if (element.tagName === "rect") {
this.rects[element.id] = <SVGRectElement>element
}
if (element.tagName === "image") {
await this.loadImage(element)
}
@ -577,6 +613,14 @@ class SvgToPdfPage {
if (element.tagName === "tspan" && element.childElementCount == 0) {
const specialValues = element.textContent.split(" ").filter((t) => t.startsWith("$"))
for (let specialValue of specialValues) {
const imageMatch = element.textContent.match(/\$img\(([^)])+\)/)
if (imageMatch) {
const key = imageMatch[1]
const url = key.startsWith("http") ? key : this.extractTranslation("${" + key + "}", `en`, false)
await this.loadImage(url)
continue
}
const importMatch = element.textContent.match(
/\$import ([a-zA-Z-_0-9.? ]+) as ([a-zA-Z0-9]+)/
)
@ -596,6 +640,7 @@ class SvgToPdfPage {
this.options.textSubstitutions
)
}
if (element.textContent.startsWith("$map(")) {
mapTextSpecs.push(<any>element)
}
@ -642,7 +687,12 @@ class SvgToPdfPage {
}
for (const mapSpec of mapSpecs) {
await this.prepareMap(mapSpec,! this.options?.disableDataLoading)
try{
await this.prepareMap(mapSpec, !this.options?.disableDataLoading)
}catch(e){
console.error("Couldn't prepare a map:", e)
}
}
}
@ -671,6 +721,10 @@ class SvgToPdfPage {
Constants.vNumber
)
}
if (text.startsWith("${") && text.endsWith("}")) {
const key = text.substring(2, text.length - 1)
return this.options.textSubstitutions[key]
}
const pathPart = text.match(/\$(([_a-zA-Z0-9? ]+\.)+[_a-zA-Z0-9? ]+)(.*)/)
if (pathPart === null) {
return text
@ -715,8 +769,8 @@ class SvgToPdfPage {
}
}
private loadImage(element: Element): Promise<void> {
const xlink = element.getAttribute("xlink:href")
private loadImage(element: Element | string): Promise<void> {
const xlink = typeof element === "string" ? element : element.getAttribute("xlink:href")
let img = document.createElement("img")
if (xlink.startsWith("data:image/svg+xml;")) {
@ -752,11 +806,11 @@ class SvgToPdfPage {
/**
* Replaces a mapSpec with the appropriate map
* @param mapSpec
* @private
*/
private async prepareMap(mapSpec: SVGTSpanElement, loadData: boolean = true): Promise<void> {
private async prepareMap(mapSpec: SVGTSpanElement, loadData: boolean): Promise<void> {
if (this.options.disableMaps) {
return
}
// Upper left point of the tspan
const {x, y} = SvgToPdfInternals.GetActualXY(mapSpec)
@ -766,11 +820,6 @@ class SvgToPdfPage {
textElement = textElement.parentElement
}
const spec = textElement.textContent
const match = spec.match(/\$map\(([^)]+)\)$/)
if (match === null) {
throw "Invalid mapspec:" + spec
}
const params = SvgToPdfInternals.parseCss(match[1], ",")
let smallestRect: SVGRectElement = undefined
let smallestSurface: number = undefined
@ -783,6 +832,7 @@ class SvgToPdfPage {
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
@ -809,95 +859,111 @@ class SvgToPdfPage {
svgImage.setAttribute("width", "" + width)
svgImage.setAttribute("height", "" + height)
let layout = AllKnownLayouts.allKnownLayouts.get(params["theme"])
if (layout === undefined) {
console.error("Could not show map with parameters", params)
throw (
"Theme not found:" + params["theme"] + ". Use theme: to define which theme to use. "
)
}
layout.widenFactor = 0
layout.overpassTimeout = 600
layout.defaultBackgroundId = params["background"] ?? layout.defaultBackgroundId
for (const paramsKey in params) {
if (paramsKey.startsWith("layer-")) {
const layerName = paramsKey.substring("layer-".length)
const key = params[paramsKey].toLowerCase().trim()
const layer = layout.layers.find((l) => l.id === layerName)
if (layer === undefined) {
throw "No layer found for " + paramsKey
}
if (key === "force") {
layer.minzoom = 0
layer.minzoomVisible = 0
}
let png: Blob
if (this.options.state !== undefined) {
png = await (new PngMapCreator(this.options.state, {
width, height,
}).CreatePng(this.options.freeComponentId))
} else {
const match = spec.match(/\$map\(([^)]*)\)$/)
if (match === null) {
throw "Invalid mapspec:" + spec
}
}
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),
lon: this.options?.overrideLocation?.lon ?? Number(params["lon"] ?? 3.717842),
})
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) {
filteredLayer.isDisplayed.setData(
loadData && params["layer-" + filteredLayer.layerDef.id].trim().toLowerCase() !== "false"
const params = SvgToPdfInternals.parseCss(match[1], ",")
let layout = AllKnownLayouts.allKnownLayouts.get(params["theme"])
if (layout === undefined) {
console.error("Could not show map with parameters", params)
throw (
"Theme not found:" + params["theme"] + ". Use theme: to define which theme to use. "
)
} else if (params["layers"] === "none") {
filteredLayer.isDisplayed.setData(false)
} else if (filteredLayer.layerDef.id.startsWith("note_import")) {
filteredLayer.isDisplayed.setData(false)
}
layout.widenFactor = 0
layout.overpassTimeout = 600
layout.defaultBackgroundId = params["background"] ?? layout.defaultBackgroundId
for (const paramsKey in params) {
if (paramsKey.startsWith("layer-")) {
const layerName = paramsKey.substring("layer-".length)
const key = params[paramsKey].toLowerCase().trim()
const layer = layout.layers.find((l) => l.id === layerName)
if (layer === undefined) {
throw "No layer found for " + paramsKey
}
if (key === "force") {
layer.minzoom = 0
layer.minzoomVisible = 0
}
}
}
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),
lon: this.options?.overrideLocation?.lon ?? Number(params["lon"] ?? 3.717842),
})
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) {
filteredLayer.isDisplayed.setData(
loadData && params["layer-" + filteredLayer.layerDef.id].trim().toLowerCase() !== "false"
)
} else if (params["layers"] === "none") {
filteredLayer.isDisplayed.setData(false)
} else if (filteredLayer.layerDef.id.startsWith("note_import")) {
filteredLayer.isDisplayed.setData(false)
}
}
for (const paramsKey in params) {
if (paramsKey.startsWith("layer-")) {
const layerName = paramsKey.substring("layer-".length)
const key = params[paramsKey].toLowerCase().trim()
const isDisplayed = loadData && (key === "true" || key === "force")
const layer = fl.find((l) => l.layerDef.id === layerName)
if (!loadData) {
console.log("Not loading map data as 'loadData' is falsed, this is probably a test run")
} else {
console.log(
"Setting ",
layer?.layerDef?.id,
" to visibility",
isDisplayed,
"(minzoom:",
layer?.layerDef?.minzoomVisible,
layer?.layerDef?.minzoom,
")"
)
}
layer.isDisplayed.setData(loadData && isDisplayed)
if (key === "force" && loadData) {
layer.layerDef.minzoom = 0
layer.layerDef.minzoomVisible = 0
layer.isDisplayed.addCallback((isDisplayed) => {
if (!isDisplayed) {
console.warn("Forcing layer " + paramsKey + " as true")
layer.isDisplayed.setData(true)
}
})
}
}
}
console.log("Creating a map width ", width, height, params.scalingFactor)
const pngCreator = new PngMapCreator(state, {
width: 4 * width,
height: 4 * height,
})
png = await pngCreator.CreatePng(this.options.freeComponentId, this._state)
if(!png){
throw "PngCreator did not output anything..."
}
}
for (const paramsKey in params) {
if (paramsKey.startsWith("layer-")) {
const layerName = paramsKey.substring("layer-".length)
const key = params[paramsKey].toLowerCase().trim()
const isDisplayed = loadData && (key === "true" || key === "force")
const layer = fl.find((l) => l.layerDef.id === layerName)
if (!loadData) {
console.log("Not loading map data as 'loadData' is falsed, this is probably a test run")
} else {
console.log(
"Setting ",
layer?.layerDef?.id,
" to visibility",
isDisplayed,
"(minzoom:",
layer?.layerDef?.minzoomVisible,
layer?.layerDef?.minzoom,
")"
)
}
layer.isDisplayed.setData(loadData && isDisplayed)
if (key === "force" && loadData) {
layer.layerDef.minzoom = 0
layer.layerDef.minzoomVisible = 0
layer.isDisplayed.addCallback((isDisplayed) => {
if (!isDisplayed) {
console.warn("Forcing layer " + paramsKey + " as true")
layer.isDisplayed.setData(true)
}
})
}
}
}
console.log("Creating a map width ", width, height, params.scalingFactor)
const pngCreator = new PngMapCreator(state, {
width: 4 * width,
height: 4 * height,
})
const png = await pngCreator.CreatePng(this._state)
svgImage.setAttribute("xlink:href", await SvgToPdfPage.blobToBase64(png))
smallestRect.parentElement.insertBefore(svgImage, smallestRect)
await this.prepareElement(svgImage, [])
@ -916,7 +982,7 @@ class SvgToPdfPage {
export class SvgToPdf {
public static readonly templates: Record<
"flyer_a4" | "poster_a3" | "poster_a2" | "current_view_a4",
"flyer_a4" | "poster_a3" | "poster_a2" | "current_view_a4" | "current_view_a3",
{ pages: string[]; description: string | Translation; isPublic: boolean }
> = {
flyer_a4: {
@ -938,8 +1004,13 @@ export class SvgToPdf {
isPublic: false
},
current_view_a4: {
pages:["./assets/templates/CurrentMapWithHeaderA4.svg"],
description: "Export a PDF (A4, portrait) of the current view",
pages: ["./assets/templates/CurrentMapWithHeaderA4.svg"],
description: "Export a PDF (A4, landscape) of the current view",
isPublic: true
},
current_view_a3: {
pages: ["./assets/templates/CurrentMapWithHeaderA3.svg"],
description: "Export a PDF (A3, portrait) of the current view",
isPublic: true
}
}
@ -965,7 +1036,10 @@ export class SvgToPdf {
this._pages = pages.map((page) => new SvgToPdfPage(page, state, options))
}
public async ConvertSvg(language: string): Promise<void> {
/**
* Construct the PDF (including the maps to create), offers them to the user to downlaod.
*/
public async ExportPdf(language: string): Promise<void> {
console.log("Building svg...")
const firstPage = this._pages[0]._svgRoot
const width = SvgToPdfInternals.attrNumber(firstPage, "width")
@ -975,13 +1049,12 @@ export class SvgToPdf {
await this.Prepare(language)
console.log("Global prepare done")
this._status.setData("Maps are rendered, building pdf")
new FixedUiElement("").AttachTo("extradiv")
console.log("Pages are prepared")
const doc = new jsPDF(mode, undefined, [width, height])
doc.advancedAPI((advancedApi) => {
const canvas = advancedApi.canvas
for (let i = 0; i < this._pages.length; i++) {
console.log("Rendering page", i)
if (i > 0) {
@ -1022,28 +1095,6 @@ export class SvgToPdf {
return allTranslations
}
/**
* Prepares all the minimaps
*/
public async Prepare(language1: string): Promise<SvgToPdf> {
for (const page of this._pages) {
await page.Prepare()
await page.PrepareLanguage(language1)
}
return this
}
public async PrepareLanguages(languages: string[]): Promise<boolean> {
for (const page of this._pages) {
// Load all languages at once.
// We don't parallelize the pages, as they'll probably reload the same languages anyway (and they are cached)
await Promise.all(
languages.map(async (language) => await page.PrepareLanguage(language))
)
}
return true
}
getTranslation(translationKey: string, language: string, strict: boolean = false) {
for (const page of this._pages) {
const tr = page.extractTranslation(translationKey, language, strict)
@ -1057,4 +1108,15 @@ export class SvgToPdf {
}
return undefined
}
/**
* Prepares all the minimaps
*/
private async Prepare(language1: string): Promise<SvgToPdf> {
for (const page of this._pages) {
await page.Prepare()
await page.PrepareLanguage(language1)
}
return this
}
}

View file

@ -0,0 +1,189 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
width="297mm"
height="420mm"
viewBox="0 0 297 420"
version="1.1"
id="svg5"
inkscape:version="1.1.2 (0a00cf5339, 2022-02-04)"
sodipodi:docname="CurrentMapWithHeaderA3.svg"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<defs
id="defs33">
<rect
x="41.54771"
y="103.43336"
width="118.76163"
height="107.62454"
id="rect19815" />
<rect
x="730.99915"
y="857.75903"
width="646.31287"
height="26.69614"
id="rect10143" />
</defs>
<sodipodi:namedview
id="namedview7"
pagecolor="#ffe1d9"
bordercolor="#111111"
borderopacity="1"
inkscape:pageshadow="0"
inkscape:pageopacity="1"
inkscape:pagecheckerboard="1"
inkscape:document-units="mm"
showgrid="false"
showguides="true"
inkscape:guide-bbox="true"
inkscape:zoom="0.48119622"
inkscape:cx="338.73915"
inkscape:cy="566.29705"
inkscape:window-width="1920"
inkscape:window-height="995"
inkscape:window-x="0"
inkscape:window-y="0"
inkscape:window-maximized="1"
inkscape:current-layer="layer2"
inkscape:snap-global="false" />
<g
inkscape:groupmode="layer"
id="layer2"
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"
id="rect27895"
width="290.34955"
height="403.45847"
x="3.7768779"
y="6.4455185"
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"
id="rect28206"
width="203.38158"
height="35.362419"
x="6.3702731"
y="9.6101332"
ry="3.858089"
rx="4.3605742" />
<text
xml:space="preserve"
transform="scale(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="tspan891"><tspan
style="font-size:13.3333px;-inkscape-font-specification:'sans-serif, Normal'"
id="tspan889">$map(current)</tspan></tspan></text>
<text
xml:space="preserve"
transform="matrix(0.26458333,0,0,0.26458333,12.08115,27.672609)"
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
style="font-size:16px;line-height:1.05;-inkscape-font-specification:'sans-serif, Normal'"
id="tspan893">$general.pdf.attr
</tspan></tspan><tspan
x="0"
y="16.799999"
id="tspan899"><tspan
style="font-size:16px;line-height:1.05;-inkscape-font-specification:'sans-serif, Normal'"
id="tspan897">$general.pdf.attrBackground
</tspan></tspan><tspan
x="0"
y="35.692733"
id="tspan905"><tspan
style="font-size:16px;line-height:1.05;-inkscape-font-specification:'sans-serif, Normal'"
id="tspan901">$general.pdf.generatedWith</tspan><tspan
style="font-size:18.6667px;line-height:1.05;-inkscape-font-specification:'sans-serif, Normal'"
id="tspan903">
</tspan></tspan></text>
<text
xml:space="preserve"
transform="scale(0.26458333)"
id="text19136"
style="font-size:16px;font-family:sans-serif;-inkscape-font-specification:'sans-serif, Normal';text-align:center;white-space:pre;shape-inside:url(#rect19138);fill:#000000;stroke:#000000;stroke-width:0.377953;stroke-linecap:round;stroke-linejoin:round;stroke-opacity:1" />
<text
xml:space="preserve"
transform="matrix(0.26458333,0,0,0.26458333,-308.35032,184.63585)"
id="text10141"
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
style="fill-opacity:1;stroke:none"
id="tspan907">$general.pdf.versionInfo</tspan></tspan></text>
</g>
<g
inkscape:label="Layer 1"
inkscape:groupmode="layer"
id="layer1"
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
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"
x="102.80793"
y="16.415634"
id="text8611-8"><tspan
sodipodi:role="line"
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:7.05556px;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;stroke-width:0.264583"
x="102.80793"
y="16.415634"
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,11.738978,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="tspan913"><tspan
style="font-weight:bold;font-size:34.6667px;-inkscape-font-specification:'sans-serif, Bold'"
id="tspan911">${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"
x="105.86118"
y="116.25558"
id="text53309"><tspan
sodipodi:role="line"
id="tspan53307"
style="stroke-width:0.264583"
x="105.86118"
y="116.25558" /><tspan
sodipodi:role="line"
style="stroke-width:0.264583"
id="tspan53311"
x="105.86118"
y="129.4847" /></text>
</g>
</svg>

After

Width:  |  Height:  |  Size: 8.1 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 56 KiB

After

Width:  |  Height:  |  Size: 8.1 KiB

View file

@ -23,9 +23,11 @@ async function testPdf() {
SvgToPdf.templates["flyer_a4"].pages.map((url) => Utils.download(url))
)
console.log("Building svg")
const pdf = new SvgToPdf("Test", svgs, {})
const pdf = new SvgToPdf("Test", svgs, {
freeComponentId:"extradiv"
})
new VariableUiElement(pdf.status).AttachTo("maindiv")
await pdf.ConvertSvg("nl")
await pdf.ExportPdf("nl")
}
testPdf().then((_) => console.log("All done"))

View file

@ -38,7 +38,6 @@
<body>
<span class="absolute" id="belowmap" style="z-index: -1; visibility: hidden">Below</span>
<div class="h-full" id="maindiv">
<div id="default-main h-full">
<div class="w-full h-screen flex flex-col items-center justify-between p-8">
@ -64,6 +63,7 @@
</div>
</div>
</div>
<div id="belowmap" style="z-index: -1">Below</div>
<script>