<?php
/* Copyright (C) 2026 Eduard Wisch <data@data-it-solution.de>
 * GPL v3+
 */

require_once DOL_DOCUMENT_ROOT.'/core/class/commonobject.class.php';

/**
 * CRUD-Klasse für einen Bericht.
 * Eine Bericht-Instanz gehört zu einem Parent-Objekt (invoice|order|propal)
 * und enthält n Seiten (BerichtPage).
 */
class Bericht extends CommonObject
{
    public $element = 'bericht';
    public $table_element = 'bericht';
    public $picto = 'fa-file-pdf';

    public $id;
    public $entity;
    public $ref;
    public $titel;
    public $element_type;     // invoice | order | propal
    public $fk_element;
    public $auftragsnummer;
    public $template_odt;
    public $page_format = 'A4';     // A4, A3, Letter
    public $page_orientation = 'P'; // P=Portrait, L=Landscape
    public $is_template = 0;        // 1 = Vorlage, wird nicht als regulärer Bericht gelistet
    public $template_label;         // Optional Label für die Vorlage
    public $version = 1;            // Version-Nummer (nur bei versionierten Berichten)
    public $fk_bericht_parent;      // Parent-Bericht bei Versionierung
    public $status;           // 0 = Entwurf, 1 = Final
    public $final_pdf_path;
    public $fk_user_creat;
    public $fk_user_modif;
    public $datec;
    public $tms;
    public $note;

    const STATUS_DRAFT = 0;
    const STATUS_FINAL = 1;

    public function __construct(DoliDB $db)
    {
        $this->db = $db;
    }

    public function create(User $user, $notrigger = 0)
    {
        global $conf;

        $this->entity   = $conf->entity;
        $this->datec    = dol_now();
        $this->status   = self::STATUS_DRAFT;
        $this->fk_user_creat = $user->id;

        if (empty($this->ref)) {
            $this->ref = 'BR'.date('ymd-His', $this->datec);
        }

        // Prüfen welche optionalen Spalten existieren (Migrationen können unvollständig sein)
        $cols = array(
            'entity' => (int) $this->entity,
            'ref' => "'".$this->db->escape($this->ref)."'",
            'titel' => ($this->titel ? "'".$this->db->escape($this->titel)."'" : "NULL"),
            'element_type' => "'".$this->db->escape($this->element_type)."'",
            'fk_element' => (int) $this->fk_element,
            'auftragsnummer' => ($this->auftragsnummer ? "'".$this->db->escape($this->auftragsnummer)."'" : "NULL"),
            'template_odt' => ($this->template_odt ? "'".$this->db->escape($this->template_odt)."'" : "NULL"),
            'status' => (int) $this->status,
            'fk_user_creat' => (int) $this->fk_user_creat,
            'datec' => "'".$this->db->idate($this->datec)."'",
            'note' => ($this->note ? "'".$this->db->escape($this->note)."'" : "NULL"),
        );
        // Optionale Spalten — nur wenn in der DB vorhanden
        $optional = array(
            'page_format' => "'".$this->db->escape($this->page_format)."'",
            'page_orientation' => "'".$this->db->escape($this->page_orientation)."'",
            'is_template' => (int) ($this->is_template ? 1 : 0),
            'template_label' => ($this->template_label ? "'".$this->db->escape($this->template_label)."'" : "NULL"),
            'version' => (int) ($this->version ?: 1),
            'fk_bericht_parent' => ($this->fk_bericht_parent ? (int) $this->fk_bericht_parent : "NULL"),
        );
        $existing = array();
        $cres = $this->db->query("SHOW COLUMNS FROM ".$this->db->prefix()."bericht");
        if ($cres) {
            while ($cr = $this->db->fetch_object($cres)) {
                $existing[$cr->Field] = true;
            }
        }
        foreach ($optional as $k => $v) {
            if (isset($existing[$k])) $cols[$k] = $v;
        }

        $sql = "INSERT INTO ".$this->db->prefix()."bericht (".implode(',', array_keys($cols)).") VALUES (".implode(',', array_values($cols)).")";

        $this->db->begin();
        $res = $this->db->query($sql);
        if (!$res) {
            $this->error = $this->db->lasterror();
            $this->db->rollback();
            return -1;
        }
        $this->id = $this->db->last_insert_id($this->db->prefix()."bericht");
        $this->db->commit();
        return $this->id;
    }

    public function fetch($id)
    {
        $sql = "SELECT rowid, entity, ref, titel, element_type, fk_element, auftragsnummer, template_odt,"
            ." page_format, page_orientation, COALESCE(is_template,0) AS is_template, template_label,"
            ." COALESCE(version,1) AS version, fk_bericht_parent,"
            ." status, final_pdf_path, fk_user_creat, fk_user_modif, datec, tms, note"
            ." FROM ".$this->db->prefix()."bericht WHERE rowid = ".((int) $id);
        $res = $this->db->query($sql);
        if (!$res) {
            $this->error = $this->db->lasterror();
            return -1;
        }
        if ($this->db->num_rows($res) == 0) {
            return 0;
        }
        $obj = $this->db->fetch_object($res);
        $this->id              = $obj->rowid;
        $this->entity          = $obj->entity;
        $this->ref             = $obj->ref;
        $this->titel           = $obj->titel;
        $this->element_type    = $obj->element_type;
        $this->fk_element      = $obj->fk_element;
        $this->auftragsnummer  = $obj->auftragsnummer;
        $this->template_odt    = $obj->template_odt;
        $this->page_format     = $obj->page_format ?: 'A4';
        $this->page_orientation= $obj->page_orientation ?: 'P';
        $this->is_template     = (int) $obj->is_template;
        $this->template_label  = $obj->template_label;
        $this->version         = (int) $obj->version;
        $this->fk_bericht_parent = $obj->fk_bericht_parent ? (int) $obj->fk_bericht_parent : null;
        $this->status          = (int) $obj->status;
        $this->final_pdf_path  = $obj->final_pdf_path;
        $this->fk_user_creat   = $obj->fk_user_creat;
        $this->fk_user_modif   = $obj->fk_user_modif;
        $this->datec           = $this->db->jdate($obj->datec);
        $this->tms             = $this->db->jdate($obj->tms);
        $this->note            = $obj->note;
        return 1;
    }

    public function update(User $user, $notrigger = 0)
    {
        $sql = "UPDATE ".$this->db->prefix()."bericht SET "
            ."titel = ".($this->titel ? "'".$this->db->escape($this->titel)."'" : "NULL").","
            ."auftragsnummer = ".($this->auftragsnummer ? "'".$this->db->escape($this->auftragsnummer)."'" : "NULL").","
            ."template_odt = ".($this->template_odt ? "'".$this->db->escape($this->template_odt)."'" : "NULL").","
            ."page_format = '".$this->db->escape($this->page_format ?: 'A4')."',"
            ."page_orientation = '".$this->db->escape($this->page_orientation ?: 'P')."',"
            ."status = ".((int) $this->status).","
            ."final_pdf_path = ".($this->final_pdf_path ? "'".$this->db->escape($this->final_pdf_path)."'" : "NULL").","
            ."note = ".($this->note ? "'".$this->db->escape($this->note)."'" : "NULL").","
            ."fk_user_modif = ".((int) $user->id)
            ." WHERE rowid = ".((int) $this->id);
        $res = $this->db->query($sql);
        if (!$res) {
            $this->error = $this->db->lasterror();
            return -1;
        }
        return 1;
    }

    public function delete(User $user, $notrigger = 0)
    {
        $this->db->begin();
        // Seiten löschen (CASCADE räumt's bereits, aber explizit für robustes Verhalten)
        $this->db->query("DELETE FROM ".$this->db->prefix()."bericht_page WHERE fk_bericht = ".((int) $this->id));
        $res = $this->db->query("DELETE FROM ".$this->db->prefix()."bericht WHERE rowid = ".((int) $this->id));
        if (!$res) {
            $this->error = $this->db->lasterror();
            $this->db->rollback();
            return -1;
        }
        $this->db->commit();
        return 1;
    }

    /**
     * Liefert alle Berichte zu einem Parent-Objekt (direkt zugeordnet).
     *
     * @param string $element_type  invoice | order | propal
     * @param int    $fk_element    ID des Parent-Objekts
     * @return Bericht[]
     */
    public static function fetchAllForElement(DoliDB $db, $element_type, $fk_element)
    {
        $list = array();
        $sql = "SELECT rowid FROM ".$db->prefix()."bericht"
            ." WHERE element_type = '".$db->escape($element_type)."'"
            ." AND fk_element = ".((int) $fk_element)
            ." AND COALESCE(is_template,0) = 0"
            ." ORDER BY datec DESC";
        $res = $db->query($sql);
        if (!$res) return $list;
        while ($obj = $db->fetch_object($res)) {
            $b = new self($db);
            if ($b->fetch($obj->rowid) > 0) {
                $list[] = $b;
            }
        }
        return $list;
    }

    /**
     * Liefert Berichte, die per llx_element_element zusätzlich zu einem Parent verknüpft wurden.
     */
    public static function fetchLinkedForElement(DoliDB $db, $targettype, $fk_target)
    {
        $list = array();
        $sql = "SELECT b.rowid FROM ".$db->prefix()."bericht b"
            ." JOIN ".$db->prefix()."element_element e ON e.fk_source = b.rowid"
            ." WHERE e.sourcetype = 'bericht'"
            ." AND e.targettype = '".$db->escape($targettype)."'"
            ." AND e.fk_target = ".((int) $fk_target)
            ." ORDER BY b.datec DESC";
        $res = $db->query($sql);
        if (!$res) return $list;
        while ($obj = $db->fetch_object($res)) {
            $b = new self($db);
            if ($b->fetch($obj->rowid) > 0) {
                $list[] = $b;
            }
        }
        return $list;
    }

    /**
     * Erzeugt eine n:m-Verknüpfung zwischen einem Bericht und einem anderen Element.
     */
    public function linkToElement($targettype, $fk_target)
    {
        // Schon vorhanden?
        $sql = "SELECT rowid FROM ".$this->db->prefix()."element_element"
            ." WHERE sourcetype = 'bericht' AND fk_source = ".((int) $this->id)
            ." AND targettype = '".$this->db->escape($targettype)."'"
            ." AND fk_target = ".((int) $fk_target);
        $res = $this->db->query($sql);
        if ($res && $this->db->num_rows($res) > 0) return 1;

        $ins = "INSERT INTO ".$this->db->prefix()."element_element"
            ." (fk_source, sourcetype, fk_target, targettype) VALUES ("
            .((int) $this->id).", 'bericht', ".((int) $fk_target).", '".$this->db->escape($targettype)."')";
        return $this->db->query($ins) ? 1 : -1;
    }

    /**
     * Entfernt eine n:m-Verknüpfung.
     */
    public function unlinkFromElement($targettype, $fk_target)
    {
        $sql = "DELETE FROM ".$this->db->prefix()."element_element"
            ." WHERE sourcetype = 'bericht' AND fk_source = ".((int) $this->id)
            ." AND targettype = '".$this->db->escape($targettype)."'"
            ." AND fk_target = ".((int) $fk_target);
        return $this->db->query($sql) ? 1 : -1;
    }

    /**
     * Liefert alle Vorlagen (is_template = 1).
     * @return Bericht[]
     */
    public static function fetchAllTemplates(DoliDB $db)
    {
        $list = array();
        $sql = "SELECT rowid FROM ".$db->prefix()."bericht"
            ." WHERE COALESCE(is_template,0) = 1"
            ." ORDER BY template_label ASC, ref ASC";
        $res = $db->query($sql);
        if (!$res) return $list;
        while ($obj = $db->fetch_object($res)) {
            $b = new self($db);
            if ($b->fetch($obj->rowid) > 0) $list[] = $b;
        }
        return $list;
    }

    /**
     * Erstellt einen neuen Bericht aus einer Vorlage:
     * Titel, Format, Orientation, Template-ODT werden übernommen;
     * alle Vorlagen-Seiten werden in den neuen Bericht kopiert (mit Notizen,
     * aber source_path wird nur übernommen wenn es ein dauerhaftes Asset ist).
     *
     * @return int|false  Neue Bericht-ID oder false
     */
    public static function createFromTemplate(DoliDB $db, User $user, $template_id, $element_type, $fk_element, $auftragsnummer)
    {
        $tpl = new self($db);
        if ($tpl->fetch($template_id) <= 0 || !$tpl->is_template) return false;

        $b = new self($db);
        $b->element_type   = $element_type;
        $b->fk_element     = $fk_element;
        $b->titel          = $tpl->titel ?: ('Bericht '.$auftragsnummer);
        $b->auftragsnummer = $auftragsnummer;
        $b->template_odt   = $tpl->template_odt;
        $b->page_format    = $tpl->page_format;
        $b->page_orientation = $tpl->page_orientation;
        $b->is_template    = 0;
        if ($b->create($user) <= 0) return false;

        // Seiten kopieren
        $tpl_pages = BerichtPage::fetchAllForBericht($db, $tpl->id);
        foreach ($tpl_pages as $order => $tp) {
            $np = new BerichtPage($db);
            $np->fk_bericht  = $b->id;
            $np->page_order  = $order + 1;
            $np->source_type = $tp->source_type;
            $np->source_path = $tp->source_path; // zeigt auf die Vorlagen-Assets (werden mitgenutzt, nicht kopiert)
            $np->source_page = $tp->source_page;
            $np->note        = $tp->note;
            $np->layout      = $tp->layout;
            $np->image_scale = $tp->image_scale;
            $np->image_align = $tp->image_align;
            $np->create();
        }
        return (int) $b->id;
    }

    /**
     * Dupliziert einen Bericht als neue Version (v+1). Seiten werden kopiert.
     * Der alte Bericht bleibt unverändert.
     *
     * @return int|false  Neue Bericht-ID
     */
    public function duplicateAsNewVersion(User $user)
    {
        $new = new Bericht($this->db);
        $new->element_type = $this->element_type;
        $new->fk_element = $this->fk_element;
        $new->auftragsnummer = $this->auftragsnummer;
        $new->template_odt = $this->template_odt;
        $new->page_format = $this->page_format;
        $new->page_orientation = $this->page_orientation;
        $new->note = $this->note;
        // Parent der Versionskette = ursprünglicher Bericht oder dessen Parent
        $new->fk_bericht_parent = $this->fk_bericht_parent ?: $this->id;
        // Höchste bisherige Version in dieser Kette finden
        $chain_id = $new->fk_bericht_parent;
        $res = $this->db->query("SELECT MAX(COALESCE(version,1)) AS v FROM ".$this->db->prefix()."bericht"
            ." WHERE rowid = ".((int) $chain_id)." OR fk_bericht_parent = ".((int) $chain_id));
        $max_v = 1;
        if ($res && ($r = $this->db->fetch_object($res))) $max_v = (int) $r->v;
        $new->version = $max_v + 1;
        $new->titel = ($this->titel ? preg_replace('/\s*\(v\d+\)$/', '', $this->titel) : 'Bericht').' (v'.$new->version.')';
        if ($new->create($user) <= 0) return false;

        // Seiten kopieren
        $src_pages = BerichtPage::fetchAllForBericht($this->db, $this->id);
        foreach ($src_pages as $order => $p) {
            $np = new BerichtPage($this->db);
            $np->fk_bericht  = $new->id;
            $np->page_order  = $order + 1;
            $np->source_type = $p->source_type;
            $np->source_path = $p->source_path;
            $np->source_page = $p->source_page;
            $np->note        = $p->note;
            $np->title       = $p->title;
            $np->rotation    = $p->rotation;
            $np->fabric_json = $p->fabric_json;
            $np->layout      = $p->layout;
            $np->image_scale = $p->image_scale;
            $np->image_align = $p->image_align;
            $np->create();
        }
        return (int) $new->id;
    }

    public function getLibStatut($mode = 0)
    {
        global $langs;
        $langs->load("bericht@bericht");
        if ($this->status == self::STATUS_FINAL) {
            return '<span class="badge badge-status4">'.$langs->trans("BerichtStatusFinal").'</span>';
        }
        return '<span class="badge badge-status0">'.$langs->trans("BerichtStatusDraft").'</span>';
    }
}

/**
 * Eine Seite eines Berichts.
 *
 * Layout-Modi:
 *   single   = ein Bild/PDF direkt auf source_path
 *   grid_2   = 2 Bilder nebeneinander (über llx_bericht_page_image)
 *   grid_4   = 2x2 Grid
 *   grid_6   = 3x2 Grid
 *
 * Bei single werden image_scale + image_align angewendet:
 *   image_scale: 1.0 / 0.7 / 0.5 / 0.3
 *   image_align: fit / center / topleft / topright / bottomleft / bottomright
 */
class BerichtPage
{
    public $db;
    public $id;
    public $fk_bericht;
    public $page_order;
    public $source_type;
    public $source_path;
    public $source_page;
    public $rotation;
    public $fabric_json;
    public $note;
    public $title;           // Optional Titel/Zwischentitel der Seite
    public $composite_path;  // Phase 6: vom Editor generiertes PNG der gesamten Seite
    public $layout = 'single';
    public $image_scale = 1.0;
    public $image_align = 'fit';

    public function __construct(DoliDB $db)
    {
        $this->db = $db;
    }

    /**
     * Liefert die Bilder einer Multi-Image-Seite (layout != single).
     * @return array  [['rowid'=>..., 'slot'=>..., 'source_path'=>..., 'rotation'=>...], ...]
     */
    public function getImages()
    {
        $list = array();
        if ($this->layout === 'single') return $list;
        $sql = "SELECT rowid, slot, source_path, rotation FROM ".$this->db->prefix()."bericht_page_image"
            ." WHERE fk_page = ".((int) $this->id)
            ." ORDER BY slot ASC";
        $res = $this->db->query($sql);
        if (!$res) return $list;
        while ($obj = $this->db->fetch_object($res)) {
            $list[] = array(
                'rowid'       => (int) $obj->rowid,
                'slot'        => (int) $obj->slot,
                'source_path' => $obj->source_path,
                'rotation'    => (int) $obj->rotation,
            );
        }
        return $list;
    }

    /**
     * Setzt das Bild eines Slots (überschreibt vorhandenes).
     */
    public function setSlotImage($slot, $source_path, $rotation = 0)
    {
        $this->db->query("DELETE FROM ".$this->db->prefix()."bericht_page_image"
            ." WHERE fk_page = ".((int) $this->id)." AND slot = ".((int) $slot));
        $sql = "INSERT INTO ".$this->db->prefix()."bericht_page_image (fk_page, slot, source_path, rotation)"
            ." VALUES (".((int) $this->id).", ".((int) $slot).", '".$this->db->escape($source_path)."', ".((int) $rotation).")";
        return $this->db->query($sql) ? 1 : -1;
    }

    public function clearImages()
    {
        return $this->db->query("DELETE FROM ".$this->db->prefix()."bericht_page_image WHERE fk_page = ".((int) $this->id)) ? 1 : -1;
    }

    /**
     * Anzahl Slots eines Layouts.
     */
    public static function slotCountForLayout($layout)
    {
        return array(
            'single'       => 1,
            'grid_2'       => 2,
            'grid_2v'      => 2,
            'grid_4'       => 4,
            'grid_6'       => 6,
            'before_after' => 2,
            'title_only'   => 0,
        )[$layout] ?? 1;
    }

    /**
     * Berechnet die Slot-Rectangles innerhalb einer Seite (mm) für ein Grid-Layout.
     * @return array  [['x'=>..,'y'=>..,'w'=>..,'h'=>..], ...]
     */
    public static function slotRects($layout, $pageW, $pageH, $margin = 10, $gap = 4)
    {
        $rects = array();
        $iw = $pageW - 2 * $margin;
        $ih = $pageH - 2 * $margin - 10; // 10mm Notiz unten

        if ($layout === 'grid_2') {
            // 2 Bilder horizontal: 1 oben, 1 unten
            $h = ($ih - $gap) / 2;
            for ($i = 0; $i < 2; $i++) {
                $rects[] = array('x' => $margin, 'y' => $margin + $i * ($h + $gap), 'w' => $iw, 'h' => $h);
            }
        } elseif ($layout === 'grid_2v') {
            // 2 Bilder vertikal nebeneinander
            $w = ($iw - $gap) / 2;
            for ($i = 0; $i < 2; $i++) {
                $rects[] = array('x' => $margin + $i * ($w + $gap), 'y' => $margin, 'w' => $w, 'h' => $ih);
            }
        } elseif ($layout === 'grid_4') {
            $w = ($iw - $gap) / 2;
            $h = ($ih - $gap) / 2;
            for ($r = 0; $r < 2; $r++) {
                for ($c = 0; $c < 2; $c++) {
                    $rects[] = array(
                        'x' => $margin + $c * ($w + $gap),
                        'y' => $margin + $r * ($h + $gap),
                        'w' => $w, 'h' => $h
                    );
                }
            }
        } elseif ($layout === 'grid_6') {
            $w = ($iw - 2 * $gap) / 3;
            $h = ($ih - $gap) / 2;
            for ($r = 0; $r < 2; $r++) {
                for ($c = 0; $c < 3; $c++) {
                    $rects[] = array(
                        'x' => $margin + $c * ($w + $gap),
                        'y' => $margin + $r * ($h + $gap),
                        'w' => $w, 'h' => $h
                    );
                }
            }
        }
        return $rects;
    }

    public function create()
    {
        $sql = "INSERT INTO ".$this->db->prefix()."bericht_page ("
            ."fk_bericht, page_order, source_type, source_path, source_page, rotation, fabric_json, note, title, layout, image_scale, image_align"
            .") VALUES ("
            .((int) $this->fk_bericht).","
            .((int) $this->page_order).","
            ."'".$this->db->escape($this->source_type)."',"
            ."'".$this->db->escape($this->source_path ?: '')."',"
            .($this->source_page !== null ? (int) $this->source_page : "NULL").","
            .((int) ($this->rotation ?? 0)).","
            .($this->fabric_json !== null ? "'".$this->db->escape($this->fabric_json)."'" : "NULL").","
            .($this->note ? "'".$this->db->escape($this->note)."'" : "NULL").","
            .($this->title ? "'".$this->db->escape($this->title)."'" : "NULL").","
            ."'".$this->db->escape($this->layout ?: 'single')."',"
            .((float) ($this->image_scale ?: 1.0)).","
            ."'".$this->db->escape($this->image_align ?: 'fit')."'"
            .")";
        $res = $this->db->query($sql);
        if (!$res) return -1;
        $this->id = $this->db->last_insert_id($this->db->prefix()."bericht_page");
        return $this->id;
    }

    public function update()
    {
        $sql = "UPDATE ".$this->db->prefix()."bericht_page SET "
            ."page_order = ".((int) $this->page_order).","
            ."rotation = ".((int) ($this->rotation ?? 0)).","
            ."fabric_json = ".($this->fabric_json !== null ? "'".$this->db->escape($this->fabric_json)."'" : "NULL").","
            ."note = ".($this->note ? "'".$this->db->escape($this->note)."'" : "NULL").","
            ."layout = '".$this->db->escape($this->layout ?: 'single')."',"
            ."image_scale = ".((float) ($this->image_scale ?: 1.0)).","
            ."image_align = '".$this->db->escape($this->image_align ?: 'fit')."'"
            ." WHERE rowid = ".((int) $this->id);
        return $this->db->query($sql) ? 1 : -1;
    }

    public function delete()
    {
        return $this->db->query("DELETE FROM ".$this->db->prefix()."bericht_page WHERE rowid = ".((int) $this->id)) ? 1 : -1;
    }

    public static function fetchAllForBericht(DoliDB $db, $fk_bericht)
    {
        $list = array();
        // composite_path kann noch fehlen falls Migration nicht durchgelaufen — defensiv
        $has_composite = false;
        $ccheck = $db->query("SHOW COLUMNS FROM ".$db->prefix()."bericht_page LIKE 'composite_path'");
        if ($ccheck && $db->num_rows($ccheck) > 0) $has_composite = true;

        $composite_col = $has_composite ? 'composite_path' : "NULL AS composite_path";
        $sql = "SELECT rowid, fk_bericht, page_order, source_type, source_path, source_page, rotation, fabric_json, note, title,"
            ." COALESCE(layout, 'single') AS layout,"
            ." COALESCE(image_scale, 1.0) AS image_scale,"
            ." COALESCE(image_align, 'fit') AS image_align,"
            ." ".$composite_col
            ." FROM ".$db->prefix()."bericht_page"
            ." WHERE fk_bericht = ".((int) $fk_bericht)
            ." ORDER BY page_order ASC, rowid ASC";
        $res = $db->query($sql);
        if (!$res) return $list;
        while ($obj = $db->fetch_object($res)) {
            $p = new self($db);
            $p->id          = $obj->rowid;
            $p->fk_bericht  = $obj->fk_bericht;
            $p->page_order  = (int) $obj->page_order;
            $p->source_type = $obj->source_type;
            $p->source_path = $obj->source_path;
            $p->source_page = $obj->source_page !== null ? (int) $obj->source_page : null;
            $p->rotation    = (int) $obj->rotation;
            $p->fabric_json = $obj->fabric_json;
            $p->note        = $obj->note;
            $p->title       = $obj->title;
            $p->composite_path = $obj->composite_path ?? null;
            $p->layout      = $obj->layout;
            $p->image_scale = (float) $obj->image_scale;
            $p->image_align = $obj->image_align;
            $list[] = $p;
        }
        return $list;
    }
}
