Skip to content

Commit

Permalink
Merge pull request #977 from digitalfabrik/900-add-field-nuernberg-pa…
Browse files Browse the repository at this point in the history
…ss-id

900: add field nuernberg pass id & add new pdf template
  • Loading branch information
sarahsporck authored May 24, 2023
2 parents 90df2d9 + 6dbc0d6 commit 48ff72c
Show file tree
Hide file tree
Showing 22 changed files with 321 additions and 174 deletions.
1 change: 0 additions & 1 deletion administration/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@ describe('CreateCardsButtonBar', () => {
id: 0,
name: 'augsburg',
prefix: 'a',
activatedForApplication: true,
}
const cards = [new CardBlueprint('Thea Test', bayernConfig.card, [region])]
const { getByText } = render(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ describe('ImportCardsInput', () => {
id: 0,
name: 'augsburg',
prefix: 'a',
activatedForApplication: true,
}

const renderAndSubmitCardsInput = async (
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ describe('useCardGenerator', () => {
id: 0,
name: 'augsburg',
prefix: 'a',
activatedForApplication: true,
}

const cards = [
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ const useCardGenerator = (region: Region) => {
})
: []

const pdfDataUri = await generatePdf(dynamicCodes, staticCodes, region, projectConfig.pdf)
const pdfDataUri = await generatePdf(dynamicCodes, staticCodes, cardBlueprints, region, projectConfig.pdf)

const codes = [...dynamicCodes, ...staticCodes]
await createCards(client, codes, region)
Expand Down
1 change: 1 addition & 0 deletions administration/src/cards/CSVCard.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ describe('CSVCard', () => {
id: 0,
name: 'augsburg',
prefix: 'a',
activatedForApplication: true,
}

const cardConfig = {
Expand Down
1 change: 1 addition & 0 deletions administration/src/cards/CardBlueprint.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ describe('CardBlueprint', () => {
id: 0,
name: 'augsburg',
prefix: 'a',
activatedForApplication: true,
}

const cardConfig = {
Expand Down
2 changes: 1 addition & 1 deletion administration/src/cards/CardBlueprint.ts
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,7 @@ export class CardBlueprint implements JSONCardBlueprint {
const extensionsMessage: PartialMessage<CardExtensions> = {}

this.extensions.forEach(extension => {
if (extension.state === null) {
if (extension.state === null || !extension.setProtobufData) {
// We allow to skip invalid extensions to enable computing the protobuf size.
return
}
Expand Down
137 changes: 24 additions & 113 deletions administration/src/cards/PdfFactory.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
import { PDFDocument, PDFFont, PDFPage, StandardFonts } from 'pdf-lib'
import { PDFDocument, PDFPage, StandardFonts } from 'pdf-lib'

import { CardInfo, DynamicActivationCode, QrCode, StaticVerificationCode } from '../generated/card_pb'
import { DynamicActivationCode, QrCode, StaticVerificationCode } from '../generated/card_pb'
import { Region } from '../generated/graphql'
import { PdfConfig } from '../project-configs/getProjectConfig'
import { drawQRCode } from '../util/qrcode'
import CardBlueprint from './CardBlueprint'
import pdfQrCodeElement from './pdf/PdfQrCodeElement'
import pdfTextElement from './pdf/pdfTextElement'

export class PDFError extends Error {
constructor(message: string) {
Expand All @@ -12,135 +14,42 @@ export class PDFError extends Error {
}
}

const dynamicQRCodeSize = 84 // mm
const dynamicQRCodeX = 108 // mm
const dynamicQRCodeY = 73 // mm

const dynamicDetailWidth = 84 // mm
const dynamicDetailX = 108 // mm
const dynamicDetailY = 170 // mm
const dynamicDetailFontSize = 10

const staticBackQRCodeSize = 48 // mm
const staticBackQRCodeX = 51 // mm
const staticBackQRCodeY = 228 // mm

const staticFrontQRCodeSize = 23 // mm
const staticFrontQRCodeX = 156 // mm
const staticFrontQRCodeY = 249 // mm

const staticDetailWidth = 46 // mm
const staticDetailX = 107 // mm
const staticDetailY = 248 // mm
const staticDetailFontSize = 8

function mmToPt(mm: number) {
return (mm / 25.4) * 72
}

type DynamicPdfQrCode = {
value: DynamicActivationCode
case: 'dynamicActivationCode'
}

type StaticPdfQrCode = {
value: StaticVerificationCode
case: 'staticVerificationCode'
}

type PdfQrCode = DynamicPdfQrCode | StaticPdfQrCode

async function fillContentAreas(
doc: PDFDocument,
templatePage: PDFPage,
dynamicCode: DynamicPdfQrCode,
staticCode: StaticPdfQrCode | null,
dynamicCode: Extract<QrCode['qrCode'], { case: 'dynamicActivationCode' }>,
staticCode: Extract<QrCode['qrCode'], { case: 'staticVerificationCode' }> | null,
region: Region,
cardBlueprint: CardBlueprint,
pdfConfig: PdfConfig
) {
const helveticaFont = await doc.embedFont(StandardFonts.Helvetica)

// Dynamic QR code
fillCodeArea(dynamicCode, dynamicQRCodeX, dynamicQRCodeY, dynamicQRCodeSize, templatePage)
fillDetailsArea(
dynamicCode.value.info!,
region,
dynamicDetailX,
dynamicDetailY,
dynamicDetailWidth,
helveticaFont,
dynamicDetailFontSize,
templatePage,
false,
pdfConfig
pdfConfig.elements?.dynamicActivationQrCodes.forEach(configOptions =>
pdfQrCodeElement(configOptions, { page: templatePage, qrCode: dynamicCode })
)

// Static QR code
if (staticCode) {
// Back
fillCodeArea(staticCode, staticBackQRCodeX, staticBackQRCodeY, staticBackQRCodeSize, templatePage)

// Front
fillCodeArea(staticCode, staticFrontQRCodeX, staticFrontQRCodeY, staticFrontQRCodeSize, templatePage)
fillDetailsArea(
staticCode.value.info!,
region,
staticDetailX,
staticDetailY,
staticDetailWidth,
helveticaFont,
staticDetailFontSize,
templatePage,
true,
pdfConfig
pdfConfig.elements?.staticVerificationQrCodes?.forEach(configOptions =>
pdfQrCodeElement(configOptions, { page: templatePage, qrCode: staticCode })
)
}
}

function fillDetailsArea(
info: CardInfo,
region: Region,
x: number,
y: number,
width: number,
font: PDFFont,
fontSize: number,
page: PDFPage,
shorten: boolean,
pdfConfig: PdfConfig
) {
const detailXPdf = mmToPt(x)
const detailYPdf = page.getSize().height - mmToPt(y)

const lineHeight = mmToPt(5)

const text = pdfConfig.infoToDetails(info, region, shorten)
page.drawText(text, {
font,
x: detailXPdf,
y: detailYPdf - lineHeight,
maxWidth: mmToPt(width),
wordBreaks: text.split('').filter(c => !'\n\f\r\u000B'.includes(c)), // Split on every character
lineHeight,
size: fontSize,
})
}

function fillCodeArea(qrCode: PdfQrCode, x: number, y: number, size: number, page: PDFPage) {
const qrCodeSizePdf = mmToPt(size)
const qrCodeXPdf = mmToPt(x)
const qrCodeYPdf = page.getSize().height - qrCodeSizePdf - mmToPt(y)

const qrCodeContent = new QrCode({
qrCode: qrCode,
}).toBinary()

drawQRCode(qrCodeContent, qrCodeXPdf, qrCodeYPdf, qrCodeSizePdf, page, false)
pdfConfig.elements?.text.forEach(configOptions =>
pdfTextElement(configOptions, {
page: templatePage,
font: helveticaFont,
info: dynamicCode.value.info!,
region: region,
cardBlueprint,
})
)
}

export async function generatePdf(
dynamicCodes: DynamicActivationCode[],
staticCodes: StaticVerificationCode[],
cardBlueprints: CardBlueprint[],
region: Region,
pdfConfig: PdfConfig
) {
Expand All @@ -159,6 +68,7 @@ export async function generatePdf(
for (let k = 0; k < dynamicCodes.length; k++) {
const dynamicCode = dynamicCodes[k]
const staticCode = staticCodes?.at(k)
const cardBlueprint = cardBlueprints[k]

const [templatePage] = templateDocument ? await doc.copyPages(templateDocument, [0]) : [null]

Expand All @@ -178,6 +88,7 @@ export async function generatePdf(
}
: null,
region,
cardBlueprint,
pdfConfig
)
}
Expand Down
69 changes: 69 additions & 0 deletions administration/src/cards/extensions/NuernbergPassIdExtension.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
import { FormGroup, InputGroup, Intent } from '@blueprintjs/core'

import { Extension } from './extensions'

const nuernbergPassIdNumberLength = 10

type NuernbergPassIdState = { nuernbergPassId: number }
class NuernbergPassIdExtension extends Extension<NuernbergPassIdState, null> {
public readonly name = NuernbergPassIdExtension.name

setInitialState() {}
createForm(onUpdate: () => void) {
return (
<FormGroup
label='Nürnberg-Pass-ID'
labelFor='nuernberg-pass-id-input'
intent={this.isValid() ? undefined : Intent.DANGER}>
<InputGroup
id='nuernberg-pass-id-input'
placeholder='12345678'
intent={this.isValid() ? undefined : Intent.DANGER}
value={this.state?.nuernbergPassId.toString() ?? ''}
maxLength={nuernbergPassIdNumberLength}
onChange={event => {
const value = event.target.value
if (value.length > nuernbergPassIdNumberLength) {
return
}

const parsedNumber = Number.parseInt(value)

if (isNaN(parsedNumber)) {
this.state = null
onUpdate()
return
}

this.state = {
nuernbergPassId: parsedNumber,
}
onUpdate()
}}
/>
</FormGroup>
)
}

causesInfiniteLifetime() {
return false
}

isValid() {
return (
this.state !== null &&
this.state.nuernbergPassId > 0 &&
this.state.nuernbergPassId < 10 ** nuernbergPassIdNumberLength
)
}

fromString(state: string) {
this.state = { nuernbergPassId: parseInt(state, 10) }
}

toString() {
return this.state ? `${this.state.nuernbergPassId}` : ''
}
}

export default NuernbergPassIdExtension
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,12 @@ class NuernbergPassNumberExtension extends Extension<NuernbergPassNumberState, n
setInitialState() {}
createForm(onUpdate: () => void) {
return (
<FormGroup label='Passnummer' labelFor='nuernberg-pass-input' intent={this.isValid() ? undefined : Intent.DANGER}>
<FormGroup
label='Nürnberg-Pass-Nummer'
labelFor='nuernberg-pass-number-input'
intent={this.isValid() ? undefined : Intent.DANGER}>
<InputGroup
id='nuernberg-pass-input'
id='nuernberg-pass-number-input'
placeholder='12345678'
intent={this.isValid() ? undefined : Intent.DANGER}
value={this.state?.passNumber.toString() ?? ''}
Expand Down
11 changes: 10 additions & 1 deletion administration/src/cards/extensions/extensions.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,17 @@ import { ReactElement } from 'react'
import { CardExtensions } from '../../generated/card_pb'
import BavariaCardTypeExtension from './BavariaCardTypeExtension'
import BirthdayExtension from './BirthdayExtension'
import NuernbergPassIdExtension from './NuernbergPassIdExtension'
import NuernbergPassNumberExtension from './NuernbergPassNumberExtension'
import RegionExtension from './RegionExtension'

export const findExtension = <E extends ExtensionClass>(
array: ExtensionInstance[],
extension: E
): InstanceType<E> | undefined => {
return array.find(e => e instanceof extension) as InstanceType<E> | undefined
}

export interface JSONExtension<T> {
name: string
state: T | null
Expand All @@ -19,7 +27,7 @@ export abstract class Extension<T, R> implements JSONExtension<T> {
abstract isValid(): boolean
abstract createForm(onChange: () => void): ReactElement | null
abstract causesInfiniteLifetime(): boolean
abstract setProtobufData(message: PartialMessage<CardExtensions>): void
setProtobufData?(message: PartialMessage<CardExtensions>): void
abstract fromString(state: string): void
abstract toString(): string
}
Expand All @@ -28,5 +36,6 @@ export type ExtensionClass =
| typeof BavariaCardTypeExtension
| typeof BirthdayExtension
| typeof NuernbergPassNumberExtension
| typeof NuernbergPassIdExtension
| typeof RegionExtension
export type ExtensionInstance = InstanceType<ExtensionClass>
13 changes: 13 additions & 0 deletions administration/src/cards/pdf/PdfElements.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
export type Coordinates = {
x: number
y: number
}

export function mmToPt(mm: number) {
return (mm / 25.4) * 72
}

export type PdfElement<ConfigOptions extends Record<string, any>, DynamicOptions extends Record<string, any>> = (
options: ConfigOptions,
dynamicOptions: DynamicOptions
) => void
Loading

0 comments on commit 48ff72c

Please sign in to comment.