From 7b3eeb65696c70be367e75a22672a6de83f9627e Mon Sep 17 00:00:00 2001 From: josee666 Date: Thu, 15 Dec 2022 09:53:36 -0500 Subject: [PATCH] feat(print): print make georeferenced PDF (#1149) * pdf_georef * lint * lint * lint * lint * lint * fontSize calculate from pageHeight * fix getExtent in 3857 * remove anlytic in angular.json --- packages/geo/src/lib/print/shared/geopdf.ts | 65 +++++ packages/geo/src/lib/print/shared/index.ts | 1 + .../src/lib/print/shared/print.interface.ts | 6 + .../geo/src/lib/print/shared/print.service.ts | 275 ++++++++++++------ 4 files changed, 265 insertions(+), 82 deletions(-) create mode 100644 packages/geo/src/lib/print/shared/geopdf.ts diff --git a/packages/geo/src/lib/print/shared/geopdf.ts b/packages/geo/src/lib/print/shared/geopdf.ts new file mode 100644 index 0000000000..bbae625857 --- /dev/null +++ b/packages/geo/src/lib/print/shared/geopdf.ts @@ -0,0 +1,65 @@ +/* + * The MIT License (MIT) + * + * Copyright (c) 2016-2017 Dan "Ducky" Little + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +export default function jsGeoPdfPlugin(jsPDFAPI) { + + const setGeoArea = function(pdfExt, geoExt) { + const bbox = pdfExt.join(' '); + // the ordering may seem odd here but PDF + // flips the Y axis upside down and this accounts for that + // change. + const minx = geoExt[0]; + const maxx = geoExt[2]; + const maxy = geoExt[1]; + const miny = geoExt[3]; + const bounds = [miny, minx, maxy, minx, maxy, maxx, miny, maxx].join(' '); + const bbox_obj = this.internal.newAdditionalObject(); + const bounds_obj = this.internal.newAdditionalObject(); + const proj_obj = this.internal.newAdditionalObject(); + + proj_obj.content = '<< /EPSG 3857 /Type /PROJCS /WKT (PROJCS["WGS_1984_Web_Mercator_Auxiliary_Sphere"'+ + ',+GEOGCS["GCS_WGS_1984",DATUM["D_WGS_1984",SPHEROID["WGS_1984",6378137.0,298.257223563]],PRIMEM["Greenwich",0.0]'+ + ',UNIT["Degree",0.017453292519943295]],PROJECTION["Mercator_Auxiliary_Sphere"],PARAMETER["False_Easting",0.0]'+ + ',PARAMETER["False_Northing",0.0],PARAMETER["Central_Meridian",0.0],PARAMETER["Standard_Parallel_1",0.0]'+ + ',PARAMETER["Auxiliary_Sphere_Type",0.0],UNIT["Meter",1.0]]) >>'; + + bounds_obj.content = '<< /Bounds [ 0 1 0 0 1 0 1 1 ] /GCS ' + + proj_obj.objId + ' 0 R /GPTS [ ' + bounds + ' ] /LPTS [ 0 1 0 0 1 0 1 1 ] /Subtype /GEO /Type /Measure >>'; + + bbox_obj.content = '<< /BBox [ ' + bbox + ' ] /Measure ' + bounds_obj.objId + ' 0 R /Name (Layer) /Type /Viewport >>'; + + const title_obj = this.internal.newAdditionalObject(); + const date = new Date().toLocaleDateString('en-CA'); + title_obj.content = '<< /Name IGO2 /Type OCG /Date '+ date +' >>'; + + return bbox_obj.objId; + }; + + jsPDFAPI.setGeoArea = function(pdfExt, geoExt) { + this.internal.events.subscribe('putPage', function() { + const bbox_id = setGeoArea.call(this, pdfExt, geoExt); + this.internal.write('/VP [ ' + bbox_id + ' 0 R ]'); + }); + }; +} diff --git a/packages/geo/src/lib/print/shared/index.ts b/packages/geo/src/lib/print/shared/index.ts index 7d4b6a4a3f..1563c98e12 100644 --- a/packages/geo/src/lib/print/shared/index.ts +++ b/packages/geo/src/lib/print/shared/index.ts @@ -1,3 +1,4 @@ export * from './print.interface'; export * from './print.service'; export * from './print.type'; +export * from './geopdf'; diff --git a/packages/geo/src/lib/print/shared/print.interface.ts b/packages/geo/src/lib/print/shared/print.interface.ts index d4fb5f956b..36fbadb140 100644 --- a/packages/geo/src/lib/print/shared/print.interface.ts +++ b/packages/geo/src/lib/print/shared/print.interface.ts @@ -23,3 +23,9 @@ export interface PrintOptions { isPrintService: boolean; doZipFile: boolean; } + +export interface TextPdfSizeAndMargin { + fontSize: number, + marginLeft: number, + height: number +} diff --git a/packages/geo/src/lib/print/shared/print.service.ts b/packages/geo/src/lib/print/shared/print.service.ts index e7589d40af..d8aab6962b 100644 --- a/packages/geo/src/lib/print/shared/print.service.ts +++ b/packages/geo/src/lib/print/shared/print.service.ts @@ -5,7 +5,7 @@ import { Observable, Subject, forkJoin } from 'rxjs'; import { map as rxMap } from 'rxjs/operators'; import { saveAs } from 'file-saver'; -import jspdf from 'jspdf'; +import jsPDF from 'jspdf'; import html2canvas from 'html2canvas'; import { default as JSZip } from 'jszip'; @@ -18,7 +18,8 @@ import { formatScale } from '../../map/shared/map.utils'; import { LegendMapViewOptions } from '../../layer/shared/layers/layer.interface'; import { getLayersLegends } from '../../layer/utils/outputLegend'; -import { PrintOptions } from './print.interface'; +import { PrintOptions, TextPdfSizeAndMargin } from './print.interface'; +import GeoPdfPlugin from './geopdf'; declare global { interface Navigator { @@ -33,6 +34,18 @@ export class PrintService { zipFile: JSZip; nbFileToProcess: number; activityId: string; + imgSizeAdded: Array; + + TEXTPDFFONT = { + titleFont: 'Times', + titleFontStyle: 'bold', + subtitleFont: 'Times', + subtitleFontStyle: 'bold', + commentFont: 'courier', + commentFontStyle: 'normal', + commentFontSize: 16 + }; + constructor( private http: HttpClient, private messageService: MessageService, @@ -43,16 +56,18 @@ export class PrintService { print(map: IgoMap, options: PrintOptions): Subject { const status$ = new Subject(); - const paperFormat: string = options.paperFormat; const resolution = +options.resolution; // Default is 96 const orientation = options.orientation; const legendPostion = options.legendPosition; - this.activityId = this.activityService.register(); - const doc = new jspdf({ + + GeoPdfPlugin(jsPDF.API); + + const doc = new jsPDF({ orientation, - format: paperFormat.toLowerCase() + format: paperFormat.toLowerCase(), + unit: 'mm' // default }); const dimensions = [ @@ -64,19 +79,52 @@ export class PrintService { const width = dimensions[0] - margins[3] - margins[1]; const height = dimensions[1] - margins[0] - margins[2]; const size = [width, height]; - let titleSizeResults = [0, 0]; + let titleSizes: TextPdfSizeAndMargin; + let subtitleSizes: TextPdfSizeAndMargin; + + // PDF title + if (options.title !== undefined ) { + const fontSizeInPt = Math.round(2 * (height + 145) * 0.05) / 2; //calculate the fontSize title from the page height. + titleSizes = this.getTextPdfObjectSizeAndMarg(options.title, + margins, + this.TEXTPDFFONT.titleFont, + fontSizeInPt, + this.TEXTPDFFONT.titleFontStyle, + doc); + + this.addTextInPdfDoc(doc, + options.title, + this.TEXTPDFFONT.titleFont, + this.TEXTPDFFONT.titleFontStyle, + titleSizes.fontSize, + titleSizes.marginLeft + margins[3], + margins[0]); + + margins[0] = titleSizes.height + margins[0]; // cumulative margin top for next elem to place in pdf doc - if (options.title !== undefined) { - titleSizeResults = this.getTitleSize(options.title, width, height, doc); // return : size(pt) and left margin (mm) - this.addTitle(doc, options.title, titleSizeResults[0], margins[3] + titleSizeResults[1], titleSizeResults[0] * (25.4 / 72)); } + + // PDF subtitle if (options.subtitle !== undefined) { - let subtitleSizeResult = 0; - const titleH = titleSizeResults[0]; - subtitleSizeResult = this.getSubTitleSize(options.subtitle, width, height, doc); // return : size(pt) and left margin (mm) - this.addSubTitle(doc, options.subtitle, titleH * 0.7, margins[3] + subtitleSizeResult, titleH * 1.7 * (25.4 / 72)); - margins[0] = margins[0] + titleSizeResults[0] * 0.7 * (25.4 / 72); + subtitleSizes = this.getTextPdfObjectSizeAndMarg(options.subtitle, + margins, + this.TEXTPDFFONT.subtitleFont, + titleSizes.fontSize * 0.7, // 70% size of title + this.TEXTPDFFONT.subtitleFontStyle, + doc); + + this.addTextInPdfDoc(doc, + options.subtitle, + this.TEXTPDFFONT.subtitleFont, + this.TEXTPDFFONT.subtitleFontStyle, + subtitleSizes.fontSize, + subtitleSizes.marginLeft + margins[3], + margins[0] + ); + + margins[0] += 5; // cumulative marg top for next elem to place in pdf doc. 5 is a fix it could be adjust } + if (options.showProjection === true || options.showScale === true) { this.addProjScale( doc, @@ -95,6 +143,12 @@ export class PrintService { if (status === SubjectStatus.Done) { await this.addScale(doc, map, margins); await this.handleMeasureLayer(doc, map, margins); + + const width = this.imgSizeAdded[0]; + const height = this.imgSizeAdded[1]; + const res = 1; + this.addGeoRef(doc, map, width, height, res, margins); + if (options.legendPosition !== 'none') { if (['topleft', 'topright', 'bottomleft', 'bottomright'].indexOf(options.legendPosition) > -1 ) { await this.addLegendSamePage(doc, map, margins, resolution, options.legendPosition); @@ -115,6 +169,24 @@ export class PrintService { return status$; } + // ref GeoMoose https://github.com/geomoose/gm3/tree/main/src/gm3/components/print + addGeoRef(doc, map, width, height, resolution, margins) { + const unit = 'mm'; + const docHeight = doc.internal.pageSize.getHeight(); + + // x,y = margin left-bottom corner for img in pdf doc + const x = margins[3]; + const y = docHeight - margins[0] - height; + + let pdf_extents = [x, y, x + width, y + height]; + for(let i = 0; i < pdf_extents.length; i++) { + pdf_extents[i] = this.pdf_units2points(pdf_extents[i], unit); + } + + const mapExtent = map.viewController.getExtent('EPSG:3857'); + doc.setGeoArea(pdf_extents, mapExtent); + + } /** * Add measure overlay on the map on the document when the measure layer is present @@ -123,7 +195,7 @@ export class PrintService { * @param margins - Page margins */ private async handleMeasureLayer( - doc: jspdf, + doc: jsPDF, map: IgoMap, margins: Array ) { @@ -267,74 +339,69 @@ export class PrintService { return status$; } - getTitleSize(title: string, pageWidth: number, pageHeight: number, doc: jspdf) { + + getTextPdfObjectSizeAndMarg(text: string, margins, font:string, fontSizeInPt: number, fontStyle:string, doc: jsPDF) + : TextPdfSizeAndMargin { const pdfResolution = 96; - const titleSize = Math.round(2 * (pageHeight + 145) * 0.05) / 2; - doc.setFont('Times', 'bold'); - const width = doc.getTextWidth(title); - - const titleWidth = doc.getStringUnitWidth(title) * titleSize / doc.internal.scaleFactor; - const titleTailleMinimale = Math.round( 2 * (pageHeight + 150 ) * 0.037) / 2; - let titleFontSize = 0; - - let titleMarginLeft; - if (titleWidth >= (pageWidth)) { - titleMarginLeft = 0; - titleFontSize = Math.round(((pageWidth / title.length) * pdfResolution) / 25.4); - // If the formula to find the font size gives below the defined minimum size - if (titleFontSize < titleTailleMinimale) { - titleFontSize = titleTailleMinimale; - } - } else { - titleMarginLeft = (pageWidth - titleWidth) / 2 ; - titleFontSize = titleSize; - } - return [titleFontSize, titleMarginLeft]; - } + const docWidth = doc.internal.pageSize.getWidth(); + const pageWidth = docWidth - margins[1] - margins[3]; - getSubTitleSize(subtitle: string, pageWidth: number, pageHeight: number, doc: jspdf) { - const subtitleSize = 0.7 * Math.round(2 * (pageHeight + 145) * 0.05) / 2; // 70% of the title's font size + // important to set it first, the textDimension change when font change! + doc.setFont(font, fontStyle); + doc.setFontSize(fontSizeInPt); - doc.setFont('Times', 'bold'); + let textDimensions = doc.getTextDimensions(text); + let textMarginLeft: number; - const subtitleWidth = doc.getStringUnitWidth(subtitle) * subtitleSize / doc.internal.scaleFactor; + if (textDimensions.w > pageWidth) { + // if the text is to long, reduce fontSize 70% and the overflow with be cut in print... + textMarginLeft = 0; + fontSizeInPt = fontSizeInPt * 0.7; + doc.setFontSize(fontSizeInPt); + textDimensions = doc.getTextDimensions(text); - let subtitleMarginLeft; - if (subtitleWidth >= (pageWidth)) { - subtitleMarginLeft = 0; } else { - subtitleMarginLeft = (pageWidth - subtitleWidth) / 2 ; + textMarginLeft = (pageWidth - textDimensions.w ) / 2 ; } - return subtitleMarginLeft; - } - private addTitle(doc: jspdf, title: string, titleFontSize: number, titleMarginLeft: number, titleMarginTop: number) { - doc.setFont('Times', 'bold'); - doc.setFontSize(titleFontSize); - doc.text(title, titleMarginLeft, titleMarginTop); + return { + 'fontSize': fontSizeInPt, + 'marginLeft': textMarginLeft, + 'height': textDimensions.h + }; } - private addSubTitle(doc: jspdf, subtitle: string, subtitleFontSize: number, subtitleMarginLeft: number, subtitleMarginTop: number) { - doc.setFont('Times', 'bold'); - doc.setFontSize(subtitleFontSize); - doc.text(subtitle, subtitleMarginLeft, subtitleMarginTop); - } /** * Add comment to the document * * @param doc - pdf document * * @param comment - Comment to add in the document - * * @param size - Size of the document */ - private addComment(doc: jspdf, comment: string) { - const commentSize = 16; - const commentMarginLeft = 20; - const marginBottom = 5; - const heightPixels = doc.internal.pageSize.height - marginBottom; - - doc.setFont('courier'); - doc.setFontSize(commentSize); - doc.text(comment, commentMarginLeft, heightPixels); + private addComment(doc: jsPDF, comment: string) { + const commentMarginLeft = 20; //margin left and bottom is fix + const commentMarginBottom = 5; + const marginTop = doc.internal.pageSize.height - commentMarginBottom; + this. addTextInPdfDoc(doc,comment, + this.TEXTPDFFONT.commentFont, + this.TEXTPDFFONT.commentFontStyle, + this.TEXTPDFFONT.commentFontSize, + commentMarginLeft, + marginTop + ); } + + private addTextInPdfDoc(doc: jsPDF, + textToAdd: string, + textFont: string, + textFontStyle: string, + textFontSize: number, + textMarginLeft: number, + textMarginTop: number) + { + doc.setFont(textFont, textFontStyle); + doc.setFontSize(textFontSize); + doc.text(textToAdd, textMarginLeft, textMarginTop); + } + /** * Add projection and/or scale to the document * @param doc - pdf document @@ -344,7 +411,7 @@ export class PrintService { * @param scale - Bool to indicate if scale need to be added */ private addProjScale( - doc: jspdf, + doc: jsPDF, map: IgoMap, dpi: number, projection: boolean, @@ -381,7 +448,7 @@ export class PrintService { * @param margins - Page margins */ private async addLegend( - doc: jspdf, + doc: jsPDF, map: IgoMap, margins: Array, resolution: number @@ -434,7 +501,7 @@ export class PrintService { * @param margins - Page margins */ private async addLegendSamePage( - doc: jspdf, + doc: jsPDF, map: IgoMap, margins: Array, resolution: number, @@ -496,7 +563,7 @@ export class PrintService { * @param margins - Page margins */ private async addScale( - doc: jspdf, + doc: jsPDF, map: IgoMap, margins: Array ) { @@ -538,7 +605,7 @@ export class PrintService { } private addCanvas( - doc: jspdf, + doc: jsPDF, canvas: HTMLCanvasElement, margins: Array ) { @@ -548,6 +615,10 @@ export class PrintService { } if (image !== undefined) { + if (image.length < 20 ) { // img is corrupt todo: fix addScale in mobile make a corrupt img + console.log('Warning: An image cannot be print in pdf file'); + return; + } const imageSize = this.getImageSizeToFitPdf(doc, canvas, margins); doc.addImage( image, @@ -558,12 +629,13 @@ export class PrintService { imageSize[1] ); doc.rect(margins[3], margins[0], imageSize[0], imageSize[1]); + this.imgSizeAdded = imageSize; // keep img size for georef later } } // TODO fix printing with image resolution private addMap( - doc: jspdf, + doc: jsPDF, map: IgoMap, resolution: number, size: Array, @@ -853,8 +925,8 @@ export class PrintService { * Save document * @param doc - Document to save */ - protected async saveDoc(doc: jspdf) { - await doc.save('map.pdf', { returnPromise: true }); + protected async saveDoc(doc: jsPDF) { + await doc.save('map_georef.pdf', { returnPromise: true }); } /** @@ -865,12 +937,11 @@ export class PrintService { */ private getImageSizeToFitPdf(doc, canvas, margins) { // Define variable to calculate best size to fit in one page - const pageHeight = - doc.internal.pageSize.getHeight() - (margins[0] + margins[2]); - const pageWidth = - doc.internal.pageSize.getWidth() - (margins[1] + margins[3]); - const canHeight = canvas.height; - const canWidth = canvas.width; + const pageHeight = doc.internal.pageSize.getHeight() - (margins[0] + margins[2]); + const pageWidth = doc.internal.pageSize.getWidth() - (margins[1] + margins[3]); + const canHeight = this.pdf_units2points(canvas.height, 'mm'); + const canWidth = this.pdf_units2points(canvas.width, 'mm'); + const heightRatio = canHeight / pageHeight; const widthRatio = canWidth / pageWidth; const maxRatio = heightRatio > widthRatio ? heightRatio : widthRatio; @@ -1009,4 +1080,44 @@ export class PrintService { delete that.zipFile; }); } + + + private pdf_units2points(n, unit): number { + let k = 1; + + // this code is borrowed from jsPDF + // as it does not expose a public API + // for converting units to points. + switch (unit) { + case 'pt': + k = 1; + break; + case 'mm': + k = 72 / 25.4; + break; + case 'cm': + k = 72 / 2.54; + break; + case 'in': + k = 72; + break; + case 'px': + k = 96 / 72; + break; + case 'pc': + k = 12; + break; + case 'em': + k = 12; + break; + case 'ex': + k = 6; + break; + default: + throw new Error('Invalid unit: ' + unit); + } + + return n * k; + } + }