403Webshell
Server IP : 202.61.199.114  /  Your IP : 216.73.217.139
Web Server : nginx/1.22.1
System : Linux de.arni-solutions.de 6.1.0-49-amd64 #1 SMP PREEMPT_DYNAMIC Debian 6.1.174-1 (2026-05-26) x86_64
User : web20 ( 1018)
PHP Version : 8.4.23
Disable Function : NONE
MySQL : OFF  |  cURL : ON  |  WGET : ON  |  Perl : ON  |  Python : OFF  |  Sudo : ON  |  Pkexec : ON
Directory :  /var/www/clients/client2/web20/web/wp-content/uploads/beipackzettel/

Upload File :
current_dir [ Writeable ] document_root [ Writeable ]

 

Command :


[ Back ]     

Current File : /var/www/clients/client2/web20/web/wp-content/uploads/beipackzettel//generate.py
#!/usr/bin/env python3
"""
Erzeugt professionelle Anwendungsanleitungen (Beipackzettel) im IVD-Stil
für alle Drogentests auf drogentest-online.de.

Orientiert am Format der nal von minden Drug-Screen Gebrauchsanweisungen
(EN ISO 18113-1 konform).

Aufruf: python3 generate.py
"""

import json, os, html, re, textwrap, tempfile
from fpdf import FPDF
import qrcode
from qrcode.image.styledpil import StyledPilImage
from PIL import Image

OUT_DIR = os.path.dirname(os.path.abspath(__file__))

# --- Brand colors ---
NAVY   = (7, 28, 58)
GREEN  = (16, 142, 108)
WHITE  = (255, 255, 255)
LGRAY  = (245, 247, 249)
MGRAY  = (139, 151, 166)
BORDER = (222, 226, 230)
RED    = (192, 57, 43)

# --- Known cut-off values per substance (standard) ---
CUTOFFS = {
    'Amphetamin':          '1.000 ng/ml',
    'Kokain':              '300 ng/ml',
    'THC / Cannabis':      '50 ng/ml',
    'MDMA / Ecstasy':      '500 ng/ml',
    'Opiate / Morphin':    '300 ng/ml',
    'Methamphetamin':      '1.000 ng/ml',
    'Benzodiazepine':      '300 ng/ml',
    'Barbiturate':         '300 ng/ml',
    'Methadon':            '300 ng/ml',
    'Trizyklische Antidepressiva': '1.000 ng/ml',
}

NACHWEISZEITEN = {
    'Amphetamin':          '1 - 4 Tage',
    'Kokain':              '1 - 4 Tage',
    'THC / Cannabis':      '3 - 30 Tage (abhängig von Konsumhäufigkeit)',
    'MDMA / Ecstasy':      '1 - 3 Tage',
    'Opiate / Morphin':    '1 - 4 Tage',
    'Methamphetamin':      '1 - 4 Tage',
    'Benzodiazepine':      '1 - 14 Tage (substanzabhängig)',
    'Barbiturate':         '1 - 7 Tage',
    'Methadon':            '2 - 7 Tage',
    'Trizyklische Antidepressiva': '2 - 7 Tage',
}

METABOLITEN = {
    'Amphetamin':          'd-Amphetamin',
    'Kokain':              'Benzoylecgonin',
    'THC / Cannabis':      '11-nor-Delta-9-THC-9-COOH',
    'MDMA / Ecstasy':      '3,4-Methylendioxymethamphetamin',
    'Opiate / Morphin':    'Morphin',
    'Methamphetamin':      'd-Methamphetamin',
    'Benzodiazepine':      'Oxazepam',
    'Barbiturate':         'Secobarbital',
    'Methadon':            'Methadon',
    'Trizyklische Antidepressiva': 'Nortriptylin',
}


def make_qr(url, size_px=600):
    """Generate a white-on-navy QR code PNG, return path to temp file."""
    qr = qrcode.QRCode(
        version=None,
        error_correction=qrcode.constants.ERROR_CORRECT_M,
        box_size=10,
        border=2,
    )
    qr.add_data(url)
    qr.make(fit=True)
    img = qr.make_image(fill_color=(255, 255, 255), back_color=NAVY)
    img = img.convert('RGBA')
    img = img.resize((size_px, size_px), Image.NEAREST)
    tmp = tempfile.NamedTemporaryFile(suffix='.png', delete=False)
    img.save(tmp.name)
    return tmp.name


ABKUERZUNGEN = {
    'Amphetamin':          'AMP',
    'Kokain':              'COC',
    'THC / Cannabis':      'THC',
    'MDMA / Ecstasy':      'MDMA',
    'Opiate / Morphin':    'MOR',
    'Methamphetamin':      'MET',
    'Benzodiazepine':      'BZO',
    'Barbiturate':         'BAR',
    'Methadon':            'MTD',
    'Trizyklische Antidepressiva': 'TCA',
}


def sub_abbrev(s):
    return ABKUERZUNGEN.get(clean(s), clean(s)[:3].upper())


def clean(s):
    """Remove HTML entities and tags."""
    s = html.unescape(s)
    s = re.sub(r'<[^>]+>', '', s)
    s = s.replace('­', '')
    return s


class IFU_PDF(FPDF):
    """Custom PDF with IVD-style layout."""

    def __init__(self, product):
        super().__init__('P', 'mm', 'A4')
        self.product = product
        self.set_margins(15, 10, 15)
        self.set_auto_page_break(auto=True, margin=18)

    def header(self):
        if self.page_no() == 1:
            return
        self.set_font('Helvetica', 'I', 7)
        self.set_text_color(*MGRAY)
        title = clean(self.product['title'])
        self.cell(0, 5, f'{title} | Anwendungsanleitung', align='R')
        self.ln(8)

    def footer(self):
        self.set_y(-14)
        self.set_font('Helvetica', '', 7)
        self.set_text_color(*MGRAY)
        self.cell(0, 5, f'Seite {self.page_no()}/{{nb}}  |  www.drogentest-online.de  |  Dokumentenstand: Juni 2026', align='C')

    def draw_header_block(self):
        """First-page header with product name and brand."""
        title = clean(self.product['title'])
        brand = self.product.get('brand', '')
        ean   = self.product.get('ean', '')
        subs  = self.product.get('substanzen', [])

        # Top bar
        self.set_fill_color(*NAVY)
        self.rect(0, 0, 210, 38, 'F')
        self.set_fill_color(*GREEN)
        self.rect(0, 38, 210, 2.5, 'F')

        # IVD symbol area
        self.set_xy(15, 6)
        self.set_font('Helvetica', 'B', 8)
        self.set_text_color(*WHITE)
        self.cell(20, 5, 'IVD', border=1, align='C')
        self.set_xy(38, 6)
        self.set_font('Helvetica', '', 7)
        self.cell(60, 5, 'In-vitro-Diagnostikum zur Eigenanwendung')

        # QR code top-right (per-product, white on navy)
        slug = self.product.get('slug', '')
        qr_url = f'https://www.drogentest-online.de/drogentest/{slug}/'
        qr_path = make_qr(qr_url)
        qr_size = 28
        self.image(qr_path, x=210 - 15 - qr_size, y=5, w=qr_size, h=qr_size)
        os.unlink(qr_path)

        # Product title
        title_w = 210 - 15 - 15 - qr_size - 4
        self.set_xy(15, 14)
        self.set_font('Helvetica', 'B', 14)
        if self.get_string_width(title) > title_w:
            self.set_font('Helvetica', 'B', 11)
        if self.get_string_width(title) > title_w:
            self.set_font('Helvetica', 'B', 9)
        self.cell(title_w, 7, title)

        # Subtitle — use abbreviations for multi-substance tests
        self.set_xy(15, 23)
        self.set_font('Helvetica', '', 9)
        self.set_text_color(200, 220, 240)
        if subs and len(subs) > 3:
            sub_label = ' | '.join([sub_abbrev(s) for s in subs])
        elif subs:
            sub_label = ' | '.join([clean(s) for s in subs])
        else:
            sub_label = 'Urin-Schnelltest'
        subtitle = f'Anwendungsanleitung  |  {sub_label}'
        if self.get_string_width(subtitle) > title_w:
            self.set_font('Helvetica', '', 7.5)
        self.cell(title_w, 5, subtitle)

        # Brand + EAN right
        self.set_xy(15, 30)
        self.set_font('Helvetica', '', 7)
        self.set_text_color(180, 195, 215)
        info = f'Hersteller: {brand}'
        if ean:
            info += f'  |  EAN: {ean}'
        self.cell(title_w, 4, info)

        self.set_y(46)

    def section_title(self, num, title):
        """Numbered section heading. Forces page break if too close to bottom."""
        if self.get_y() > self.h - self.b_margin - 30:
            self.add_page()
        self.set_font('Helvetica', 'B', 10)
        self.set_text_color(*NAVY)
        self.set_fill_color(*LGRAY)
        self.cell(0, 7, f'  {num}.  {title}', fill=True)
        self.ln(9)

    def body_text(self, text):
        """Regular body paragraph."""
        self.set_font('Helvetica', '', 8.5)
        self.set_text_color(40, 40, 40)
        self.multi_cell(0, 4.5, text)
        self.ln(2)

    def bullet(self, text):
        """Bullet point."""
        self.set_font('Helvetica', '', 8.5)
        self.set_text_color(40, 40, 40)
        x = self.get_x()
        self.cell(6, 4.5, '-  ')
        self.multi_cell(0, 4.5, text)
        self.ln(0.5)

    def warning_box(self, text):
        """Red warning/disclaimer box."""
        W = 180  # full content width
        self.set_fill_color(253, 237, 236)
        self.set_draw_color(*RED)
        x = self.l_margin
        y = self.get_y()
        text_w = W - 14

        # Calculate height
        self.set_xy(x + 2, y + 2)
        self.set_font('Helvetica', 'B', 8)
        self.set_text_color(*RED)
        self.cell(8, 4.5, '/!\\')
        self.set_font('Helvetica', '', 8)
        self.set_text_color(80, 30, 20)
        self.multi_cell(text_w, 4.2, text)
        end_y = self.get_y() + 2

        # Draw box around
        self.rect(x, y, W, end_y - y, 'D')
        self.set_fill_color(253, 237, 236)
        self.rect(x + 0.3, y + 0.3, W - 0.6, end_y - y - 0.6, 'F')

        # Re-draw text on top of fill
        self.set_xy(x + 4, y + 3)
        self.set_font('Helvetica', 'B', 8)
        self.set_text_color(*RED)
        self.cell(6, 4.5, '/!\\')
        self.set_font('Helvetica', '', 8)
        self.set_text_color(80, 30, 20)
        self.multi_cell(text_w, 4.2, text)
        self.ln(4)

    def info_box(self, label, value):
        """Key-value info line."""
        self.set_font('Helvetica', 'B', 8.5)
        self.set_text_color(*NAVY)
        self.cell(45, 5, label + ':')
        self.set_font('Helvetica', '', 8.5)
        self.set_text_color(40, 40, 40)
        self.cell(0, 5, value)
        self.ln(5.5)

    def step(self, num, text):
        """Numbered procedure step."""
        self.set_font('Helvetica', 'B', 9)
        self.set_text_color(*GREEN)
        self.cell(8, 5, str(num))
        self.set_font('Helvetica', '', 8.5)
        self.set_text_color(40, 40, 40)
        self.multi_cell(0, 4.5, text)
        self.ln(1.5)

    def cutoff_table(self, substances, ngwert):
        """Table of substances with cut-off values."""
        W = 180  # full content width
        c1, c2, c3, c4 = 50, 42, 28, W - 50 - 42 - 28  # 60mm last col
        self.set_font('Helvetica', 'B', 8)
        self.set_fill_color(*NAVY)
        self.set_text_color(*WHITE)
        self.cell(c1, 6, '  Substanz', fill=True)
        self.cell(c2, 6, 'Metabolit', fill=True)
        self.cell(c3, 6, 'Cut-off', fill=True, align='C')
        self.cell(c4, 6, 'Nachweiszeit (ca.)', fill=True, align='C')
        self.ln()

        self.set_font('Helvetica', '', 7.5)
        self.set_text_color(40, 40, 40)
        alt = False
        for sub in substances:
            s = clean(sub)
            if alt:
                self.set_fill_color(*LGRAY)
            else:
                self.set_fill_color(*WHITE)
            alt = not alt

            cutoff = CUTOFFS.get(s, '-')
            if len(ngwert) == 1 and len(substances) == 1:
                cutoff = clean(ngwert[0])
            metab = METABOLITEN.get(s, '-')
            nwz = NACHWEISZEITEN.get(s, '-')

            self.cell(c1, 5.5, f'  {s}', fill=True)
            self.cell(c2, 5.5, metab[:26], fill=True)
            self.cell(c3, 5.5, cutoff, fill=True, align='C')
            self.cell(c4, 5.5, nwz, fill=True, align='C')
            self.ln()

        self.set_draw_color(*BORDER)
        self.line(self.l_margin, self.get_y(), self.w - self.r_margin, self.get_y())
        self.ln(4)


def is_multi(product):
    return len(product.get('substanzen', [])) > 1

def is_cassette(product):
    t = clean(product['title']).lower()
    return 'kassette' in t or 'testkass' in t or 'testkas' in t or 'card' in t or 'kassetten' in t

def is_strip(product):
    t = clean(product['title']).lower()
    return 'teststreifen' in t or 'teststr.' in t

def is_diptest(product):
    t = clean(product['title']).lower()
    return 'tauchtest' in t or 'multi dip' in t or 'dip' in t

def test_format_label(product):
    if is_cassette(product):
        return 'Testkassette'
    if is_diptest(product):
        return 'Tauchtest'
    return 'Teststreifen'


def generate_pdf(product):
    pdf = IFU_PDF(product)
    pdf.alias_nb_pages()
    pdf.add_page()
    pdf.draw_header_block()

    title = clean(product['title'])
    brand = product.get('brand', '')
    ean   = product.get('ean', '')
    subs  = [clean(s) for s in product.get('substanzen', [])]
    ngwert = [clean(n) for n in product.get('ngwert', [])]
    multi = is_multi(product)
    fmt   = test_format_label(product)

    if multi:
        sub_count = len(subs)
        sub_list = ', '.join(subs)
    else:
        sub_list = subs[0] if subs else 'Drogenmetabolite'

    # 1. VERWENDUNGSZWECK
    pdf.section_title(1, 'Verwendungszweck')
    if multi:
        pdf.body_text(
            f'Der {title} ist ein immunochromatographischer Schnelltest '
            f'zum qualitativen Nachweis von {sub_count} Substanzgruppen '
            f'({sub_list}) und deren Hauptmetaboliten im menschlichen Urin. '
            f'Der Test dient der In-vitro-Diagnostik und ist zur Eigenanwendung bestimmt. '
            f'Ein positives Testergebnis ist ein Vortest und muss durch ein '
            f'laboranalytisches Verfahren (z. B. GC-MS) bestätigt werden.'
        )
    else:
        cutoff_info = ''
        if ngwert:
            cutoff_info = f' mit einem Cut-off von {ngwert[0]}'
        pdf.body_text(
            f'Der {title} ist ein immunochromatographischer Schnelltest '
            f'zum qualitativen Nachweis von {sub_list}{cutoff_info} '
            f'im menschlichen Urin. '
            f'Der Test dient der In-vitro-Diagnostik und ist zur Eigenanwendung bestimmt. '
            f'Ein positives Testergebnis ist ein Vortest und muss durch ein '
            f'laboranalytisches Verfahren (z. B. GC-MS) bestätigt werden.'
        )

    # 2. TESTPRINZIP
    pdf.section_title(2, 'Testprinzip')
    if fmt == 'Teststreifen':
        pdf.body_text(
            'Der Test basiert auf dem Prinzip der kompetitiven Immunoassay-Technik '
            '(Lateral-Flow-Immunochromatographie). In der Testmembran sind '
            'spezifische Antikörper immobilisiert. Wird eine drogenhaltige Urinprobe '
            'aufgetragen, konkurrieren die Drogenmetabolite mit farbstoffmarkierten '
            'Antigen-Konjugaten um die Bindungsstellen. Bei Abwesenheit der Droge '
            'bindet das Konjugat an die Antikörper und erzeugt eine sichtbare '
            'Testlinie (T). In Gegenwart der Droge wird die Bindung verhindert '
            'und die Testlinie erscheint nicht oder nur schwach. '
            'Eine Kontrolllinie (C) bestätigt die korrekte Testdurchführung.'
        )
    else:
        pdf.body_text(
            'Der Test basiert auf dem Prinzip der kompetitiven Immunoassay-Technik '
            '(Lateral-Flow-Immunochromatographie). In der Testmembran sind für '
            f'jeden der {len(subs)} Parameter spezifische Antikörper immobilisiert. '
            'Wird eine drogenhaltige Urinprobe aufgetragen, konkurrieren die '
            'Drogenmetabolite mit farbstoffmarkierten Antigen-Konjugaten um die '
            'Bindungsstellen. Für jeden Parameter entsteht ein eigenes Linienpaar '
            'aus Kontrolllinie (C) und Testlinie (T). '
            'Eine Kontrolllinie (C) bestätigt die korrekte Testdurchführung.'
        )

    # 3. NACHGEWIESENE SUBSTANZEN / CUT-OFF-WERTE
    pdf.section_title(3, 'Nachgewiesene Substanzen und Cut-off-Werte')
    pdf.cutoff_table(subs, ngwert)
    pdf.body_text(
        'Der Cut-off-Wert gibt die Mindestkonzentration an, ab der der Test '
        'ein positives Ergebnis anzeigt. Konzentrationen unterhalb des Cut-off '
        'werden als negativ gewertet. Die angegebenen Nachweiszeiten sind '
        'Richtwerte und können individuell abweichen.'
    )

    # 4. INHALT
    pdf.section_title(4, 'Packungsinhalt')
    if fmt == 'Teststreifen':
        pdf.bullet(f'1 einzeln versiegelter {fmt}')
    elif fmt == 'Testkassette':
        pdf.bullet(f'1 einzeln versiegelte {fmt}')
        pdf.bullet('1 Einweg-Pipette (je nach Hersteller)')
    else:
        pdf.bullet(f'1 einzeln versiegelter {fmt}')
    pdf.bullet('1 Anwendungsanleitung')
    if is_cassette(product):
        pdf.bullet('1 Trockenmittelbeutel (nicht öffnen, nicht verschlucken)')
    pdf.ln(2)

    # 5. LAGERUNG UND HALTBARKEIT
    pdf.section_title(5, 'Lagerung und Haltbarkeit')
    pdf.bullet('Bei +2 °C bis +30 °C trocken und lichtgeschützt lagern.')
    pdf.bullet('Nicht einfrieren.')
    pdf.bullet('Erst unmittelbar vor Gebrauch aus der Folienverpackung entnehmen.')
    pdf.bullet('Das Haltbarkeitsdatum auf der Verpackung beachten. Nach Ablauf nicht mehr verwenden.')
    pdf.bullet('Trockenmittelbeutel nicht öffnen und nicht verschlucken.')
    pdf.ln(2)

    # 6. PROBENVORBEREITUNG
    pdf.section_title(6, 'Probenvorbereitung')
    pdf.body_text(
        'Verwenden Sie frischen, unkonservierten Urin (Mittelstrahlurin). '
        'Die Probe sollte eine Temperatur von +15 °C bis +30 °C aufweisen. '
        'Wird die Probe nicht sofort getestet, kann sie bei +2 °C bis +8 °C '
        'bis zu 48 Stunden aufbewahrt werden. Für längere Lagerung kann '
        'die Probe bei -20 °C eingefroren werden; vor dem Test vollständig '
        'auf Raumtemperatur aufwärmen und gut mischen.'
    )
    pdf.body_text(
        'Sichtbar trübe, stark verfärbte oder kontaminierte Proben können '
        'das Ergebnis beeinflussen und sollten verworfen werden.'
    )

    # 7. TESTDURCHFUEHRUNG
    pdf.section_title(7, 'Testdurchführung')

    if fmt == 'Teststreifen':
        pdf.step(1, 'Entnehmen Sie den Teststreifen aus der versiegelten Folienverpackung. Verwenden Sie ihn innerhalb von 30 Minuten.')
        pdf.step(2, 'Tauchen Sie den Teststreifen mit der Pfeilseite nach unten in den aufgefangenen Urin. Achten Sie darauf, dass der Urin die MAX-Markierung nicht überschreitet.')
        pdf.step(3, 'Halten Sie den Streifen 10 bis 15 Sekunden im Urin.')
        pdf.step(4, 'Entnehmen Sie den Streifen und legen Sie ihn waagerecht auf eine saubere, trockene, nicht saugende Unterlage.')
        pdf.step(5, 'Lesen Sie das Ergebnis nach 5 Minuten ab. Bewerten Sie das Ergebnis nicht nach mehr als 10 Minuten.')
    elif fmt == 'Testkassette':
        pdf.step(1, 'Entnehmen Sie die Testkassette und die Pipette aus der Verpackung.')
        pdf.step(2, 'Sammeln Sie die Urinprobe in einem sauberen, trockenen Behälter.')
        pdf.step(3, 'Entnehmen Sie mit der Pipette etwas Urin und geben Sie 2 bis 3 Tropfen in die Probenvertiefung (S) der Kassette.')
        pdf.step(4, 'Starten Sie die Zeitmessung.')
        pdf.step(5, 'Lesen Sie das Ergebnis nach 5 Minuten ab. Bewerten Sie das Ergebnis nicht nach mehr als 10 Minuten.')
    else:
        pdf.step(1, 'Entnehmen Sie den Tauchtest aus der versiegelten Folienverpackung.')
        pdf.step(2, 'Entfernen Sie die Schutzkappe am unteren Ende des Tauchtests.')
        pdf.step(3, 'Tauchen Sie den Test mit den Teststreifen nach unten in den aufgefangenen Urin. Der Urin darf die MAX-Markierung nicht überschreiten.')
        pdf.step(4, 'Halten Sie den Test 10 bis 15 Sekunden im Urin.')
        pdf.step(5, 'Entnehmen Sie den Test und legen Sie ihn waagerecht auf eine saubere Unterlage. Lesen Sie das Ergebnis nach 5 bis 10 Minuten ab.')

    # 8. ERGEBNIS ABLESEN
    pdf.section_title(8, 'Ergebnis ablesen')

    # NEGATIV
    pdf.set_font('Helvetica', 'B', 9)
    pdf.set_text_color(*GREEN)
    pdf.cell(0, 5, 'NEGATIV (kein Drogennachweis)')
    pdf.ln(6)
    pdf.set_font('Helvetica', '', 8.5)
    pdf.set_text_color(40, 40, 40)
    if multi:
        pdf.body_text(
            'Es erscheinen zwei Linien: eine Kontrolllinie (C) und eine Testlinie (T) '
            'für jeden Parameter. Auch eine schwache Testlinie ist als negativ zu werten. '
            'Negativ bedeutet, dass die Konzentration der jeweiligen Substanz unterhalb '
            'des Cut-off-Werts liegt oder nicht vorhanden ist.'
        )
    else:
        pdf.body_text(
            'Es erscheinen zwei Linien: eine Kontrolllinie (C) und eine Testlinie (T). '
            'Auch eine schwache Testlinie ist als negativ zu werten. '
            'Negativ bedeutet, dass die Konzentration der Substanz unterhalb '
            'des Cut-off-Werts liegt oder nicht vorhanden ist.'
        )

    # POSITIV
    pdf.set_font('Helvetica', 'B', 9)
    pdf.set_text_color(*RED)
    pdf.cell(0, 5, 'POSITIV (Drogennachweis)')
    pdf.ln(6)
    pdf.set_font('Helvetica', '', 8.5)
    pdf.set_text_color(40, 40, 40)
    if multi:
        pdf.body_text(
            'Die Kontrolllinie (C) erscheint, aber für einen oder mehrere Parameter '
            'fehlt die Testlinie (T). Der jeweilige Parameter ist dann als positiv zu werten. '
            'Ein positives Ergebnis ist ein Hinweis und muss durch eine Laboranalyse '
            '(GC-MS oder gleichwertiges Verfahren) bestätigt werden.'
        )
    else:
        pdf.body_text(
            'Es erscheint nur die Kontrolllinie (C). Die Testlinie (T) fehlt. '
            'Ein positives Ergebnis ist ein Hinweis und muss durch eine Laboranalyse '
            '(GC-MS oder gleichwertiges Verfahren) bestätigt werden.'
        )

    # UNGÜLTIG
    pdf.set_font('Helvetica', 'B', 9)
    pdf.set_text_color(180, 120, 0)
    pdf.cell(0, 5, 'UNGÜLTIG')
    pdf.ln(6)
    pdf.set_font('Helvetica', '', 8.5)
    pdf.set_text_color(40, 40, 40)
    pdf.body_text(
        'Es erscheint keine Kontrolllinie (C). Der Test ist ungültig und muss '
        'mit einem neuen Test wiederholt werden. Mögliche Ursachen: unzureichende '
        'Probenmenge, falsche Durchführung oder beschädigter Test.'
    )

    # Result diagram
    W = 180
    lm = pdf.l_margin
    pdf.set_fill_color(*LGRAY)
    y0 = pdf.get_y()
    pdf.rect(lm, y0, W, 32, 'F')
    pdf.set_draw_color(*BORDER)
    pdf.rect(lm, y0, W, 32, 'D')

    # Three result boxes
    box_w = 52
    gap = (W - 3 * box_w) / 4
    for i, (label, c_vis, t_vis, color) in enumerate([
        ('NEGATIV', True, True, GREEN),
        ('POSITIV', True, False, RED),
        ('UNGÜLTIG', False, False, (180, 120, 0)),
    ]):
        bx = lm + gap + i * (box_w + gap)
        by = y0 + 3
        # Mini box
        pdf.set_draw_color(*NAVY)
        pdf.rect(bx, by, box_w, 24, 'D')
        # Label
        pdf.set_xy(bx, by + 1)
        pdf.set_font('Helvetica', 'B', 7)
        pdf.set_text_color(*color)
        pdf.cell(box_w, 4, label, align='C')
        # C line
        pdf.set_xy(bx + 5, by + 7)
        pdf.set_font('Helvetica', '', 6.5)
        pdf.set_text_color(*NAVY)
        pdf.cell(6, 3, 'C')
        line_end = bx + box_w - 6
        if c_vis:
            pdf.set_draw_color(*NAVY)
            pdf.set_line_width(0.6)
            pdf.line(bx + 12, by + 8.5, line_end, by + 8.5)
        else:
            pdf.set_draw_color(*BORDER)
            pdf.set_line_width(0.2)
            pdf.set_font('Helvetica', 'I', 5.5)
            pdf.set_text_color(*MGRAY)
            pdf.set_xy(bx + 12, by + 7)
            pdf.cell(line_end - bx - 12, 3, '(keine Linie)', align='C')
        # T line
        pdf.set_xy(bx + 5, by + 14)
        pdf.set_font('Helvetica', '', 6.5)
        pdf.set_text_color(*NAVY)
        pdf.cell(6, 3, 'T')
        if t_vis:
            pdf.set_draw_color(*GREEN)
            pdf.set_line_width(0.4)
            pdf.line(bx + 12, by + 15.5, line_end, by + 15.5)
        else:
            pdf.set_draw_color(*BORDER)
            pdf.set_line_width(0.2)
            pdf.set_font('Helvetica', 'I', 5.5)
            pdf.set_text_color(*MGRAY)
            pdf.set_xy(bx + 12, by + 14)
            pdf.cell(line_end - bx - 12, 3, '(keine Linie)', align='C')
        # Bottom label
        pdf.set_xy(bx, by + 20)
        pdf.set_font('Helvetica', '', 5.5)
        pdf.set_text_color(80, 80, 80)
        if label == 'NEGATIV':
            pdf.cell(box_w, 3, 'C + T sichtbar', align='C')
        elif label == 'POSITIV':
            pdf.cell(box_w, 3, 'nur C sichtbar', align='C')
        else:
            pdf.cell(box_w, 3, 'keine C-Linie', align='C')

    pdf.set_line_width(0.2)
    pdf.set_y(y0 + 36)

    # 9. QUALITAETSKONTROLLE
    pdf.section_title(9, 'Qualitätskontrolle')
    pdf.body_text(
        'Der Test enthält eine eingebaute Verfahrenskontrolle. Die Kontrolllinie (C) '
        'dient als interne Positivkontrolle und bestätigt, dass ausreichend Probenvolumen '
        'aufgetragen wurde und die Membran korrekt durchfeuchtet ist. '
        'Erscheint keine Kontrolllinie, ist das Ergebnis ungültig.'
    )

    # 10. EINSCHRAENKUNGEN
    pdf.section_title(10, 'Einschränkungen und Hinweise')
    pdf.bullet('Dieser Test ist ein immunochromatographischer Schnelltest (Vortest). Positive Ergebnisse müssen durch ein laboranalytisches Verfahren (GC-MS, LC-MS/MS) bestätigt werden.')
    pdf.bullet('Der Test liefert ein qualitatives Ergebnis. Eine Quantifizierung der Substanzkonzentration ist nicht möglich.')
    pdf.bullet('Bestimmte Medikamente, Nahrungsmittel oder Nahrungsergänzungsmittel können zu Kreuzreaktionen und damit zu falsch-positiven Ergebnissen führen.')
    pdf.bullet('Stark verdünnter Urin (z. B. durch hohe Flüssigkeitszufuhr) kann zu falsch-negativen Ergebnissen führen.')
    pdf.bullet('Der Test ist nicht geeignet, um den Grad einer Beeinträchtigung oder die aufgenommene Menge zu bestimmen.')
    pdf.bullet('Verfallene oder beschädigte Tests nicht verwenden.')
    pdf.ln(2)

    # 11. ENTSORGUNG
    pdf.section_title(11, 'Entsorgung')
    pdf.body_text(
        'Gebrauchte Tests, Pipetten und Urinproben als potenziell infektiöses Material '
        'behandeln. Die Entsorgung erfolgt gemäß den örtlichen Vorschriften für '
        'biologische Abfälle bzw. über den Restmüll in einem geschlossenen Beutel. '
        'Nicht in die Kanalisation geben.'
    )

    # 12. SYMBOLERLAEUTERUNG
    pdf.section_title(12, 'Symbolerläuterung')
    symbols = [
        ('IVD',   'In-vitro-Diagnostikum'),
        ('LOT',   'Chargenbezeichnung (siehe Verpackung)'),
        ('REF',   'Bestellnummer / Artikelnummer'),
        ('USE BY','Verwendbar bis (Haltbarkeitsdatum)'),
        ('+2..+30 °C', 'Lagertemperatur'),
        ('1x',    'Nur zur einmaligen Verwendung'),
    ]
    for sym, desc in symbols:
        pdf.set_font('Helvetica', 'B', 8)
        pdf.set_text_color(*NAVY)
        pdf.cell(22, 5, sym)
        pdf.set_font('Helvetica', '', 8)
        pdf.set_text_color(40, 40, 40)
        pdf.cell(0, 5, desc)
        pdf.ln(5.5)
    pdf.ln(3)

    # DISCLAIMER
    pdf.section_title(13, 'Rechtlicher Hinweis')
    pdf.warning_box(
        'Dieses Dokument ist eine allgemeine Anwendungsanleitung, erstellt von '
        'Drogentest-Online.de. Es handelt sich nicht um die offizielle Packungsbeilage '
        'des Herstellers. Die maßgebliche Gebrauchsanweisung liegt dem Produkt '
        'in der Originalverpackung bei und ist stets vorrangig zu beachten. '
        'Drogentest-Online.de übernimmt keine Haftung für Abweichungen '
        'zwischen dieser Anleitung und der herstellerseitigen Dokumentation.'
    )

    pdf.body_text(
        'Dieser Schnelltest ersetzt keine ärztliche Diagnostik oder Beratung. '
        'Ein positives Ergebnis sollte immer durch ein laboranalytisches '
        'Bestätigungsverfahren (z. B. GC-MS) verifiziert werden, bevor '
        'daraus Konsequenzen abgeleitet werden. Bei Fragen zu Substanzkonsum, '
        'Gesundheitsrisiken oder Hilfsangeboten wenden Sie sich an eine '
        'Suchtberatungsstelle (www.drogentest-online.de/suchtberatung/).'
    )

    pdf.ln(3)
    pdf.set_font('Helvetica', '', 7)
    pdf.set_text_color(*MGRAY)
    pdf.cell(0, 4, f'Hersteller des Produkts: {brand}', ln=True)
    if ean:
        pdf.cell(0, 4, f'EAN: {ean}', ln=True)
    pdf.cell(0, 4, 'Dokumentenherausgeber: Drogentest-Online.de | Stand: Juni 2026', ln=True)
    pdf.cell(0, 4, 'Dieses Dokument darf nicht ohne schriftliche Genehmigung reproduziert werden.', ln=True)

    # Save
    slug = product['slug']
    filename = f'{slug}-anleitung.pdf'
    filepath = os.path.join(OUT_DIR, filename)
    pdf.output(filepath)
    return filename


def main():
    data_file = os.path.join(OUT_DIR, 'products.json')
    if not os.path.exists(data_file):
        print(f'ERROR: {data_file} not found. Export product data first.')
        return

    with open(data_file, 'r') as f:
        products = json.load(f)

    results = []
    for p in products:
        try:
            fn = generate_pdf(p)
            print(f'  OK  {fn}')
            results.append({'id': p['id'], 'slug': p['slug'], 'file': fn})
        except Exception as e:
            print(f'  ERR {p["slug"]}: {e}')

    print(f'\n{len(results)} PDFs generiert in {OUT_DIR}/')


if __name__ == '__main__':
    main()

Youez - 2016 - github.com/yon3zu
LinuXploit