declare(strict_types=1); function ensure_affiliation_schema(): void { static $done=false; if($done) return; $done=true; try{ $pdo=db(); $pdo->exec("CREATE TABLE IF NOT EXISTS affiliation_settings ( tenant_id INT UNSIGNED NOT NULL, enabled TINYINT(1) NOT NULL DEFAULT 0, menu_label VARCHAR(120) NOT NULL DEFAULT 'Filiação', page_title VARCHAR(190) NOT NULL DEFAULT 'Filiação Sindical', intro_text TEXT NULL, affiliation_text LONGTEXT NULL, receiver_email VARCHAR(190) NULL, document_mode VARCHAR(20) NOT NULL DEFAULT 'manual', template_pdf_path VARCHAR(255) NULL, template_pages_json LONGTEXT NULL, success_text TEXT NULL, updated_at DATETIME NULL, PRIMARY KEY(tenant_id), CONSTRAINT fk_aff_settings_tenant FOREIGN KEY(tenant_id) REFERENCES tenants(id) ON DELETE CASCADE ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci"); // Atualiza instalações existentes sem exigir importação manual. try { $c=$pdo->query("SHOW COLUMNS FROM affiliation_settings LIKE 'affiliation_text'")->fetchColumn(); if(!$c) $pdo->exec("ALTER TABLE affiliation_settings ADD COLUMN affiliation_text LONGTEXT NULL AFTER intro_text"); $dm=$pdo->query("SHOW COLUMNS FROM affiliation_settings LIKE 'document_mode'")->fetchColumn(); if(!$dm){ $pdo->exec("ALTER TABLE affiliation_settings ADD COLUMN document_mode VARCHAR(20) NOT NULL DEFAULT 'manual' AFTER receiver_email"); $pdo->exec("UPDATE affiliation_settings SET document_mode='pdf' WHERE template_pdf_path IS NOT NULL AND TRIM(template_pdf_path)<>''"); } } catch(Throwable $e) {} $pdo->exec("CREATE TABLE IF NOT EXISTS affiliation_fields ( id INT UNSIGNED NOT NULL AUTO_INCREMENT, tenant_id INT UNSIGNED NOT NULL, field_key VARCHAR(120) NOT NULL, label VARCHAR(190) NOT NULL, field_type ENUM('text','email','tel','date','select','radio','checkbox','textarea','signature','hidden') NOT NULL DEFAULT 'text', placeholder VARCHAR(255) NULL, options_json LONGTEXT NULL, required TINYINT(1) NOT NULL DEFAULT 0, page_no INT NOT NULL DEFAULT 1, pos_x DECIMAL(7,3) NOT NULL DEFAULT 10.000, pos_y DECIMAL(7,3) NOT NULL DEFAULT 10.000, box_w DECIMAL(7,3) NOT NULL DEFAULT 30.000, box_h DECIMAL(7,3) NOT NULL DEFAULT 3.000, font_size DECIMAL(6,2) NOT NULL DEFAULT 11.00, sort_order INT NOT NULL DEFAULT 0, active TINYINT(1) NOT NULL DEFAULT 1, created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, updated_at DATETIME NULL, PRIMARY KEY(id), UNIQUE KEY uq_aff_field(tenant_id,field_key), KEY idx_aff_fields(tenant_id,active,sort_order), CONSTRAINT fk_aff_fields_tenant FOREIGN KEY(tenant_id) REFERENCES tenants(id) ON DELETE CASCADE ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci"); $pdo->exec("CREATE TABLE IF NOT EXISTS affiliation_submissions ( id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT, tenant_id INT UNSIGNED NOT NULL, public_token VARCHAR(64) NOT NULL, applicant_name VARCHAR(190) NULL, applicant_email VARCHAR(190) NULL, applicant_phone VARCHAR(80) NULL, data_json LONGTEXT NOT NULL, signature_path VARCHAR(255) NULL, generated_pdf_path VARCHAR(255) NULL, signed_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, ip_address VARCHAR(64) NULL, user_agent VARCHAR(500) NULL, audit_hash VARCHAR(64) NOT NULL, email_sent TINYINT(1) NOT NULL DEFAULT 0, status ENUM('received','reviewed','approved','rejected') NOT NULL DEFAULT 'received', created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY(id), UNIQUE KEY uq_aff_token(public_token), KEY idx_aff_submissions(tenant_id,created_at), CONSTRAINT fk_aff_submissions_tenant FOREIGN KEY(tenant_id) REFERENCES tenants(id) ON DELETE CASCADE ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci"); // Pré-configuração solicitada para o STSPMH. $t=$pdo->query("SELECT id FROM tenants WHERE slug='stspmh' LIMIT 1")->fetchColumn(); if($t){ $tid=(int)$t; try{$pdo->prepare("UPDATE affiliation_settings SET document_mode='pdf' WHERE tenant_id=? AND template_pdf_path LIKE '%stspmh-ficha-filiacao-2026.pdf%'")->execute([$tid]);}catch(Throwable $e){} $q=$pdo->prepare("SELECT COUNT(*) FROM affiliation_settings WHERE tenant_id=?");$q->execute([$tid]); if(!(int)$q->fetchColumn()){ $pdo->prepare("INSERT INTO affiliation_settings(tenant_id,enabled,menu_label,page_title,intro_text,affiliation_text,receiver_email,document_mode,template_pdf_path,template_pages_json,success_text,updated_at) VALUES(?,1,'Filiação','Filiação Sindical','Preencha a ficha abaixo, confira os dados e assine digitalmente. O documento final será gerado em PDF e enviado ao sindicato.',?,'adm@stspmh.com','pdf','uploads/affiliation/templates/stspmh-ficha-filiacao-2026.pdf',?, 'Sua ficha de filiação foi recebida. Você pode baixar e imprimir o PDF assinado.',NOW())") ->execute([$tid,"Requeiro a minha filiação junto ao STSPMH – Sindicato dos Trabalhadores do Serviço Público Municipal de Hortolândia, autorizando a realização do desconto em folha de pagamento, débito em conta corrente ou por outros meios empregados pela Entidade Sindical, realizado mensalmente, hoje definida em R$ 29,00 (vinte e nove reais), a título de contribuição mensal na forma estabelecidos nos estatutos desta entidade sindical. Declaro ter conhecimento dos reajustes anuais e da importância da minha participação nas Assembleias, que dentre outras coisas, podem votar os valores a serem descontados a título de mensalidade sindical.\n\nCaso não haja saldo ou ocorra algum erro no débito, autorizo o STSPMH a reincluir o débito no mês subsequente. Responsabilizo-me pelas informações e declaro ainda que o cancelamento desta autorização só ocorra com o conhecimento do STSPMH, depois de liquidada todas as obrigações de minha responsabilidade junto ao mesmo. Este instrumento da Entidade Sindical servirá para Servidores ligados a Prefeitura Municipal de Hortolândia (CNPJ: 67.995.027/0001-32), Câmara Municipal de Hortolândia (CNPJ: 68.008.895/0001-44), e Instituto de Previdência – HORTOPREV (CNPJ: 01.335.616/0001-86), de acordo com a lotação do Servidor Público e só deixará de ter validade, após pedido formal de desfiliação da entidade e quitar integralmente quaisquer obrigações advindas de minha filiação.",json_encode(['uploads/affiliation/templates/stspmh-page-1.png'],JSON_UNESCAPED_SLASHES)]); } // Garante o termo oficial do STSPMH também em bancos já instalados. $pdo->prepare("UPDATE affiliation_settings SET affiliation_text=? WHERE tenant_id=? AND (affiliation_text IS NULL OR TRIM(affiliation_text)='')") ->execute(["Requeiro a minha filiação junto ao STSPMH – Sindicato dos Trabalhadores do Serviço Público Municipal de Hortolândia, autorizando a realização do desconto em folha de pagamento, débito em conta corrente ou por outros meios empregados pela Entidade Sindical, realizado mensalmente, hoje definida em R$ 29,00 (vinte e nove reais), a título de contribuição mensal na forma estabelecidos nos estatutos desta entidade sindical. Declaro ter conhecimento dos reajustes anuais e da importância da minha participação nas Assembleias, que dentre outras coisas, podem votar os valores a serem descontados a título de mensalidade sindical.\n\nCaso não haja saldo ou ocorra algum erro no débito, autorizo o STSPMH a reincluir o débito no mês subsequente. Responsabilizo-me pelas informações e declaro ainda que o cancelamento desta autorização só ocorra com o conhecimento do STSPMH, depois de liquidada todas as obrigações de minha responsabilidade junto ao mesmo. Este instrumento da Entidade Sindical servirá para Servidores ligados a Prefeitura Municipal de Hortolândia (CNPJ: 67.995.027/0001-32), Câmara Municipal de Hortolândia (CNPJ: 68.008.895/0001-44), e Instituto de Previdência – HORTOPREV (CNPJ: 01.335.616/0001-86), de acordo com a lotação do Servidor Público e só deixará de ter validade, após pedido formal de desfiliação da entidade e quitar integralmente quaisquer obrigações advindas de minha filiação.",$tid]); $q=$pdo->prepare("SELECT COUNT(*) FROM affiliation_fields WHERE tenant_id=?");$q->execute([$tid]); if(!(int)$q->fetchColumn()){ $fields=[ // Coordenadas calibradas sobre a ficha oficial STSPMH 2026 (1489x2105). // pos_x começa depois do rótulo; pos_y posiciona o texto sobre a linha correta. ['nome','Nome','text','',1,1,21.7,19.90,64.4,2.2,12,10], ['cpf','CPF','text','000.000.000-00',1,1,20.5,22.00,17.0,2.2,11.5,20], ['rg','RG','text','',1,1,45.8,22.00,17.0,2.2,11.5,30], ['orgao_exp','Órgão Exp.','text','',0,1,76.6,22.00,10.5,2.2,11.5,40], ['sexo','Sexo','radio','',1,1,19.5,24.20,11.0,2.2,11.5,50,json_encode(['M','F','O'])], ['celular','Celular','tel','',1,1,48.5,24.20,38.0,2.2,11.5,60], ['email','E-mail','email','',1,1,20.8,26.35,65.0,2.2,11.5,70], ['endereco','Endereço','text','',1,1,22.8,28.45,63.0,2.2,11.5,80], ['numero','Nº','text','',1,1,18.4,30.64,18.0,2.2,11.5,90], ['bairro','Bairro','text','',1,1,47.2,30.64,17.0,2.2,11.5,100], ['cidade','Cidade','text','',1,1,73.0,30.64,13.5,2.2,11.5,110], ['matricula','Matrícula','text','',1,1,22.8,32.80,14.0,2.2,11.5,120], ['local_trabalho','Local de Trabalho','text','',1,1,54.0,32.80,32.0,2.2,11.5,130], ['data_assinatura','Data','hidden','',0,1,67.0,71.10,19.0,2.2,11.5,140], ['assinatura','Assinatura do Servidor','signature','',1,1,52.0,74.60,31.0,3.8,12,150], ]; $ins=$pdo->prepare("INSERT INTO affiliation_fields(tenant_id,field_key,label,field_type,placeholder,required,page_no,pos_x,pos_y,box_w,box_h,font_size,sort_order,options_json,active,created_at) VALUES(?,?,?,?,?,?,?,?,?,?,?,?,?,?,1,NOW())"); foreach($fields as $f){$ins->execute([$tid,$f[0],$f[1],$f[2],$f[3],$f[4],$f[5],$f[6],$f[7],$f[8],$f[9],$f[10],$f[11],$f[12]??null]);} } // Migração v20: corrige SOMENTE as coordenadas antigas padrão do STSPMH. // Se o sindicato já reposicionou manualmente um campo, ele é preservado. $coordFixes=[ ['nome',14.8,21.35,21.7,19.90,64.4,2.2,12], ['cpf',14.8,23.55,20.5,22.00,17.0,2.2,11.5], ['rg',41.0,23.55,45.8,22.00,17.0,2.2,11.5], ['orgao_exp',69.0,23.55,76.6,22.00,10.5,2.2,11.5], ['sexo',23.0,25.75,19.5,24.20,11.0,2.2,11.5], ['celular',41.0,25.75,48.5,24.20,38.0,2.2,11.5], ['email',14.8,27.95,20.8,26.35,65.0,2.2,11.5], ['endereco',14.8,30.10,22.8,28.45,63.0,2.2,11.5], ['numero',14.8,32.35,18.4,30.64,18.0,2.2,11.5], ['bairro',41.0,32.35,47.2,30.64,17.0,2.2,11.5], ['cidade',69.0,32.35,73.0,30.64,13.5,2.2,11.5], ['matricula',14.8,34.55,22.8,32.80,14.0,2.2,11.5], ['local_trabalho',41.0,34.55,54.0,32.80,32.0,2.2,11.5], ['data_assinatura',66.0,72.45,67.0,71.10,19.0,2.2,11.5], ['assinatura',51.5,78.20,52.0,74.60,31.0,3.8,12], ]; $fix=$pdo->prepare("UPDATE affiliation_fields SET pos_x=?,pos_y=?,box_w=?,box_h=?,font_size=?,updated_at=NOW() WHERE tenant_id=? AND field_key=? AND ABS(pos_x-?)<0.06 AND ABS(pos_y-?)<0.06"); foreach($coordFixes as $c){$fix->execute([$c[3],$c[4],$c[5],$c[6],$c[7],$tid,$c[0],$c[1],$c[2]]);} // Migração v21: afasta os valores dos rótulos e aumenta a fonte do modelo STSPMH. $v21=[ ['nome',20.1,19.90,21.7,19.90,64.4,2.2,12],['cpf',18.6,22.00,20.5,22.00,17.0,2.2,11.5], ['rg',45.0,22.00,45.8,22.00,17.0,2.2,11.5],['orgao_exp',73.8,22.00,76.6,22.00,10.5,2.2,11.5], ['sexo',19.5,24.20,19.5,24.20,11.0,2.2,11.5],['celular',46.5,24.20,48.5,24.20,38.0,2.2,11.5], ['email',19.2,26.35,20.8,26.35,65.0,2.2,11.5],['endereco',21.0,28.45,22.8,28.45,63.0,2.2,11.5], ['numero',17.2,30.64,18.4,30.64,18.0,2.2,11.5],['bairro',46.0,30.64,47.2,30.64,17.0,2.2,11.5], ['cidade',71.5,30.64,73.0,30.64,13.5,2.2,11.5],['matricula',21.0,32.80,22.8,32.80,14.0,2.2,11.5], ['local_trabalho',52.5,32.80,54.0,32.80,32.0,2.2,11.5],['data_assinatura',66.8,71.10,67.0,71.10,19.0,2.2,11.5], ['assinatura',52.0,75.20,52.0,74.60,31.0,3.8,12] ]; $fix21=$pdo->prepare("UPDATE affiliation_fields SET pos_x=?,pos_y=?,box_w=?,box_h=?,font_size=?,updated_at=NOW() WHERE tenant_id=? AND field_key=? AND ABS(pos_x-?)<0.06 AND ABS(pos_y-?)<0.06"); foreach($v21 as $c){$fix21->execute([$c[3],$c[4],$c[5],$c[6],$c[7],$tid,$c[0],$c[1],$c[2]]);} } }catch(Throwable $e){/* instalação não deve derrubar o site */} } function affiliation_settings(int $tenantId): array { ensure_affiliation_schema(); $q=db()->prepare("SELECT * FROM affiliation_settings WHERE tenant_id=?");$q->execute([$tenantId]); return $q->fetch() ?: ['tenant_id'=>$tenantId,'enabled'=>0,'menu_label'=>'Filiação','page_title'=>'Filiação Sindical','receiver_email'=>'']; } function affiliation_fields(int $tenantId,bool $activeOnly=true): array { ensure_affiliation_schema(); $sql="SELECT * FROM affiliation_fields WHERE tenant_id=?".($activeOnly?" AND active=1":"")." ORDER BY sort_order,id"; $q=db()->prepare($sql);$q->execute([$tenantId]);return $q->fetchAll(); } function affiliation_safe_key(string $s): string { $s=slugify($s); return str_replace('-','_',$s) ?: 'campo_'.bin2hex(random_bytes(3)); } function affiliation_template_pages(array $settings): array { $x=json_decode((string)($settings['template_pages_json']??''),true); return is_array($x)?array_values(array_filter($x,'is_string')):[]; } function affiliation_render_pdf_pages(string $pdfRel,int $tenantId): array { $abs=__DIR__.'/../'.ltrim($pdfRel,'/'); if(!is_file($abs)) return []; $dir=__DIR__.'/../uploads/affiliation/templates/t'.$tenantId; if(!is_dir($dir)) @mkdir($dir,0775,true); foreach(glob($dir.'/page-*.png')?:[] as $old) @unlink($old); $prefix=$dir.'/page'; $cmd='pdftoppm -png -r 160 '.escapeshellarg($abs).' '.escapeshellarg($prefix).' 2>&1'; if(function_exists('shell_exec')) @shell_exec($cmd); $files=glob($dir.'/page-*.png')?:[]; natsort($files);$out=[]; foreach($files as $f)$out[]=str_replace(__DIR__.'/../','',$f); return array_values($out); } function affiliation_detect_common_fields(string $pdfRel,int $tenantId): int { $abs=__DIR__.'/../'.ltrim($pdfRel,'/'); if(!is_file($abs)||!function_exists('shell_exec')) return 0; $tmp=tempnam(sys_get_temp_dir(),'affbbox_'); if(!$tmp) return 0; @unlink($tmp);$xml=$tmp.'.html'; @shell_exec('pdftotext -bbox '.escapeshellarg($abs).' '.escapeshellarg($xml).' 2>/dev/null'); if(!is_file($xml)) return 0; $raw=@file_get_contents($xml);@unlink($xml); if(!$raw) return 0; libxml_use_internal_errors(true);$sx=@simplexml_load_string($raw);libxml_clear_errors();if(!$sx)return 0; $defs=['nome'=>['Nome','text'],'cpf'=>['CPF','text'],'rg'=>['RG','text'],'email'=>['E-mail','email'],'celular'=>['Celular','tel'],'endereco'=>['Endereço','text'],'bairro'=>['Bairro','text'],'cidade'=>['Cidade','text'],'matricula'=>['Matrícula','text'],'assinatura'=>['Assinatura','signature']]; $existing=[];foreach(affiliation_fields($tenantId,false) as $f)$existing[$f['field_key']]=1; $count=0;$pageNo=0; $pageNodes=$sx->xpath('//*[local-name()="page"]') ?: []; foreach($pageNodes as $page){ $pageNo++;$pw=(float)$page['width'];$ph=(float)$page['height']; $wordNodes=$page->xpath('./*[local-name()="word"]') ?: []; foreach($wordNodes as $w){ $txt=trim((string)$w); foreach($defs as $key=>$def){ if(isset($existing[$key]))continue; $norm=mb_strtolower(rtrim($txt,':.'),'UTF-8');$want=mb_strtolower($def[0],'UTF-8'); if($norm!==$want)continue; $x=((float)$w['xMax']/$pw*100)+0.8;$y=((float)$w['yMin']/$ph*100)-0.2; $ins=db()->prepare("INSERT INTO affiliation_fields(tenant_id,field_key,label,field_type,required,page_no,pos_x,pos_y,box_w,box_h,font_size,sort_order,active,created_at) VALUES(?,?,?,?,?,?,?,?,?,?,?,?,1,NOW())"); $ins->execute([$tenantId,$key,$def[0],$def[1],in_array($key,['nome','cpf','email','assinatura'],true)?1:0,$pageNo,min(90,$x),max(1,$y),30,3,10,100+$count]); $existing[$key]=1;$count++; } } } return $count; } function affiliation_store_signature(string $dataUrl,int $tenantId): ?string { if(!preg_match('~^data:image/(png|jpeg);base64,(.+)$~s',$dataUrl,$m)) return null; $bin=base64_decode($m[2],true);if($bin===false||strlen($bin)>5*1024*1024)return null; $dir=__DIR__.'/../uploads/affiliation/signatures/t'.$tenantId;if(!is_dir($dir))@mkdir($dir,0775,true); $rel='uploads/affiliation/signatures/t'.$tenantId.'/'.bin2hex(random_bytes(12)).'.png';@file_put_contents(__DIR__.'/../'.$rel,$bin);return $rel; } function affiliation_upload_signature(string $field,int $tenantId): ?string { if(empty($_FILES[$field]['tmp_name'])||!is_uploaded_file($_FILES[$field]['tmp_name']))return null; $info=@getimagesize($_FILES[$field]['tmp_name']);if(!$info||!in_array($info[2],[IMAGETYPE_PNG,IMAGETYPE_JPEG,IMAGETYPE_WEBP],true))return null; $dir=__DIR__.'/../uploads/affiliation/signatures/t'.$tenantId;if(!is_dir($dir))@mkdir($dir,0775,true); $ext=$info[2]===IMAGETYPE_PNG?'png':($info[2]===IMAGETYPE_WEBP?'webp':'jpg');$rel='uploads/affiliation/signatures/t'.$tenantId.'/'.bin2hex(random_bytes(12)).'.'.$ext;move_uploaded_file($_FILES[$field]['tmp_name'],__DIR__.'/../'.$rel);return $rel; } function affiliation_draw_wrapped(Imagick $im, ImagickDraw $draw, string $text, int $x, int &$y, int $maxWidth, int $lineHeight): void { $words=preg_split('/\s+/u',trim($text))?:[];$line=''; foreach($words as $word){$test=$line===''?$word:$line.' '.$word;$m=$im->queryFontMetrics($draw,$test);if(($m['textWidth']??0)>$maxWidth && $line!==''){$im->annotateImage($draw,$x,$y,0,$line);$y+=$lineHeight;$line=$word;}else{$line=$test;}} if($line!==''){$im->annotateImage($draw,$x,$y,0,$line);$y+=$lineHeight;} } function affiliation_make_manual_pdf(int $tenantId,array $settings,array $fields,array $data,?string $signatureRel,string $token): string { $w=1240;$h=1754;$margin=90;$tmpDir=sys_get_temp_dir().'/aff_manual_'.$token;@mkdir($tmpDir,0775,true);$jpgs=[];$pageNo=0; $tenantName='Sindicato';try{$q=db()->prepare('SELECT name FROM tenants WHERE id=?');$q->execute([$tenantId]);$tenantName=(string)($q->fetchColumn()?:'Sindicato');}catch(Throwable $e){} $newPage=function(string $subtitle='') use($w,$h,$margin,$tenantName,$settings,&$pageNo){$pageNo++;$im=new Imagick();$im->newImage($w,$h,'white');$im->setImageColorspace(Imagick::COLORSPACE_RGB);$d=new ImagickDraw();$d->setFillColor('#000000');$d->setTextAntialias(true);$d->setFontSize(28);$d->setFontWeight(700);$im->annotateImage($d,$margin,90,0,$tenantName);$d->setFontSize(36);$im->annotateImage($d,$margin,145,0,(string)($settings['page_title']??'Filiação Sindical'));if($subtitle!==''){$d->setFontSize(18);$d->setFontWeight(400);$im->annotateImage($d,$margin,180,0,$subtitle);}return [$im,$d,$subtitle===''?220:215];}; $save=function(Imagick $im) use(&$jpgs,$tmpDir,&$pageNo){$jpg=$tmpDir.'/p'.$pageNo.'.jpg';$im->setImageFormat('jpeg');$im->setImageCompressionQuality(95);$im->writeImage($jpg);$im->clear();$jpgs[]=$jpg;}; [$im,$draw,$y]=$newPage();$draw->setFontSize(22); foreach($fields as $f){if(!(int)$f['active']||$f['field_type']==='signature'||$f['field_type']==='hidden')continue;if($y>1500){$save($im);[$im,$draw,$y]=$newPage('Continuação dos dados do formulário');$draw->setFontSize(22);} $v=trim((string)($data[$f['field_key']]??''));$label=(string)$f['label'];$draw->setFontWeight(700);$im->annotateImage($draw,$margin,$y,0,$label.':');$draw->setFontWeight(400);$maxValueW=$w-$margin*2-330;if($f['field_type']==='textarea' || mb_strlen($v)>55){$yy=$y;affiliation_draw_wrapped($im,$draw,$v,$margin+320,$yy,$maxValueW,30);$y=max($y+48,$yy+8);}else{$im->annotateImage($draw,$margin+320,$y,0,$v);$y+=48;}} $term=trim((string)($settings['affiliation_text']??''));if($term!==''){if($y>1180){$save($im);[$im,$draw,$y]=$newPage('Termo de filiação');}else{$y+=28;$draw->setFontWeight(700);$draw->setFontSize(25);$im->annotateImage($draw,$margin,$y,0,'Termo de filiação');$y+=42;}$draw->setFontWeight(400);$draw->setFontSize(19);foreach(preg_split('/\n\s*\n/u',$term)?:[] as $p){$words=preg_split('/\s+/u',trim($p))?:[];$line='';foreach($words as $word){$test=$line===''?$word:$line.' '.$word;$m=$im->queryFontMetrics($draw,$test);if(($m['textWidth']??0)>($w-$margin*2) && $line!==''){if($y>1500){$save($im);[$im,$draw,$y]=$newPage('Termo de filiação — continuação');$draw->setFontSize(19);$draw->setFontWeight(400);} $im->annotateImage($draw,$margin,$y,0,$line);$y+=28;$line=$word;}else{$line=$test;}}if($line!==''){if($y>1500){$save($im);[$im,$draw,$y]=$newPage('Termo de filiação — continuação');$draw->setFontSize(19);$draw->setFontWeight(400);} $im->annotateImage($draw,$margin,$y,0,$line);$y+=28;}$y+=16;}} if($y>1450){$save($im);[$im,$draw,$y]=$newPage('Assinatura');}$y=max($y+40,1380);$draw->setFillColor('#000000');$draw->setFontSize(20);$draw->setFontWeight(400);$im->annotateImage($draw,$margin,$y,0,'Data: '.date('d/m/Y'));$im->annotateImage($draw,690,$y,0,'Assinatura do filiado:');$im->drawImage($draw); if($signatureRel){$sa=__DIR__.'/../'.ltrim($signatureRel,'/');if(is_file($sa)){$sig=new Imagick($sa);$sig->setImageBackgroundColor('transparent');$sig->thumbnailImage(380,120,true,true);$im->compositeImage($sig,Imagick::COMPOSITE_OVER,780,$y-105);$sig->clear();}} $save($im);if(!$jpgs)throw new RuntimeException('Não foi possível montar o documento.');$outDir=__DIR__.'/../storage/affiliations/t'.$tenantId;if(!is_dir($outDir))@mkdir($outDir,0775,true);$rel='storage/affiliations/t'.$tenantId.'/filiacao-'.$token.'.pdf';affiliation_jpegs_to_pdf($jpgs,__DIR__.'/../'.$rel);foreach($jpgs as $f)@unlink($f);@rmdir($tmpDir);return $rel; } function affiliation_make_pdf(int $tenantId,array $settings,array $fields,array $data,?string $signatureRel,string $token): string { $pages=affiliation_template_pages($settings); if(!class_exists('Imagick')) throw new RuntimeException('A extensão Imagick é necessária para gerar o documento final.'); if(($settings['document_mode']??'manual')!=='pdf' || !$pages) return affiliation_make_manual_pdf($tenantId,$settings,$fields,$data,$signatureRel,$token); $jpegPages=[];$tmpDir=sys_get_temp_dir().'/aff_'.$token;@mkdir($tmpDir,0775,true); foreach($pages as $idx=>$rel){$abs=__DIR__.'/../'.ltrim($rel,'/');if(!is_file($abs))continue;$im=new Imagick($abs);$im->setImageColorspace(Imagick::COLORSPACE_RGB);$w=$im->getImageWidth();$h=$im->getImageHeight(); foreach($fields as $f){if(!(int)$f['active']||(int)$f['page_no']!==$idx+1)continue;$key=$f['field_key'];$val=(string)($data[$key]??'');if($key==='data_assinatura'&&$val==='')$val=date('d/m/Y');$x=$w*((float)$f['pos_x']/100);$y=$h*((float)$f['pos_y']/100);$bw=$w*((float)$f['box_w']/100);$bh=$h*((float)$f['box_h']/100); if($f['field_type']==='signature' && $signatureRel){$sa=__DIR__.'/../'.ltrim($signatureRel,'/');if(is_file($sa)){$sig=new Imagick($sa);$sig->setImageBackgroundColor('transparent');$sig->thumbnailImage((int)$bw,(int)$bh,true,true);$sx=(int)($x+max(0,($bw-$sig->getImageWidth())/2));$sy=(int)($y+max(0,($bh-$sig->getImageHeight())/2));$im->compositeImage($sig,Imagick::COMPOSITE_OVER,$sx,$sy);$sig->clear();}continue;} // Na ficha oficial STSPMH, Sexo já possui M/F/O impressos: marque X no parêntese correto. $isStspmh=str_contains((string)($settings['template_pdf_path']??''),'stspmh-ficha-filiacao-2026.pdf'); if($isStspmh && $key==='sexo' && $val!==''){ $markX=['M'=>19.55,'F'=>24.05,'O'=>28.55][strtoupper(trim($val))]??null; if($markX!==null){$draw=new ImagickDraw();$draw->setFillColor('#000000');$draw->setFontSize(max(16,11*($w/850)));$draw->setTextAntialias(true);$im->annotateImage($draw,(int)($w*$markX/100),(int)($y+$bh*.72),0,'X');$draw->clear();} continue; } if($val==='')continue;$draw=new ImagickDraw();$draw->setFillColor('#000000');$draw->setFontSize(max(15,(float)$f['font_size']*($w/850)));$draw->setTextAntialias(true);$text=$val;if($f['field_type']==='checkbox')$text=in_array(strtolower($val),['1','sim','yes','on'],true)?'X':'';$im->annotateImage($draw,(int)$x,(int)($y+$bh*.72),0,$text);$draw->clear(); } $jpg=$tmpDir.'/p'.($idx+1).'.jpg';$im->setImageFormat('jpeg');$im->setImageCompressionQuality(92);$im->writeImage($jpg);$im->clear();$jpegPages[]=$jpg; } if(!$jpegPages) throw new RuntimeException('Não foi possível montar as páginas do documento.'); $outDir=__DIR__.'/../storage/affiliations/t'.$tenantId;if(!is_dir($outDir))@mkdir($outDir,0775,true);$rel='storage/affiliations/t'.$tenantId.'/filiacao-'.$token.'.pdf';affiliation_jpegs_to_pdf($jpegPages,__DIR__.'/../'.$rel);foreach($jpegPages as $f)@unlink($f);@rmdir($tmpDir);return $rel; } function affiliation_jpegs_to_pdf(array $jpgs,string $out): void { $objs=[];$pages=[];$n=1;$catalog=1;$pagesObj=2;$n=3; foreach($jpgs as $jpg){$info=getimagesize($jpg);if(!$info)continue;$data=file_get_contents($jpg);$w=$info[0];$h=$info[1];$page=$n++;$content=$n++;$image=$n++;$pages[]=$page;$pageW=595.28;$pageH=841.89;$objs[$page]="<< /Type /Page /Parent $pagesObj 0 R /MediaBox [0 0 $pageW $pageH] /Resources << /XObject << /Im0 $image 0 R >> >> /Contents $content 0 R >>";$stream="q\n$pageW 0 0 $pageH 0 0 cm\n/Im0 Do\nQ\n";$objs[$content]="<< /Length ".strlen($stream)." >>\nstream\n$stream\nendstream";$objs[$image]="<< /Type /XObject /Subtype /Image /Width $w /Height $h /ColorSpace /DeviceRGB /BitsPerComponent 8 /Filter /DCTDecode /Length ".strlen($data)." >>\nstream\n".$data."\nendstream";} $objs[$catalog]="<< /Type /Catalog /Pages $pagesObj 0 R >>";$objs[$pagesObj]="<< /Type /Pages /Kids [".implode(' ',array_map(fn($p)=>$p.' 0 R',$pages))."] /Count ".count($pages)." >>";ksort($objs);$pdf="%PDF-1.4\n%\xE2\xE3\xCF\xD3\n";$offsets=[0];foreach($objs as $id=>$body){$offsets[$id]=strlen($pdf);$pdf.="$id 0 obj\n$body\nendobj\n";}$xref=strlen($pdf);$max=max(array_keys($objs));$pdf.="xref\n0 ".($max+1)."\n0000000000 65535 f \n";for($i=1;$i<=$max;$i++)$pdf.=sprintf('%010d 00000 n ',(int)($offsets[$i]??0))."\n";$pdf.="trailer\n<< /Size ".($max+1)." /Root 1 0 R >>\nstartxref\n$xref\n%%EOF";file_put_contents($out,$pdf); } function affiliation_mail_pdf(string $to,string $subject,string $body,string $pdfAbs,string $filename): bool { if(!filter_var($to,FILTER_VALIDATE_EMAIL)||!is_file($pdfAbs))return false;$boundary='=_Sindsites_'.bin2hex(random_bytes(10));$headers="MIME-Version: 1.0\r\nContent-Type: multipart/mixed; boundary=\"$boundary\"\r\n";$msg="--$boundary\r\nContent-Type: text/plain; charset=UTF-8\r\nContent-Transfer-Encoding: 8bit\r\n\r\n$body\r\n";$bin=chunk_split(base64_encode(file_get_contents($pdfAbs)));$msg.="--$boundary\r\nContent-Type: application/pdf; name=\"$filename\"\r\nContent-Transfer-Encoding: base64\r\nContent-Disposition: attachment; filename=\"$filename\"\r\n\r\n$bin\r\n--$boundary--";return @mail($to,$subject,$msg,$headers); }