Compartir: imagen automática para redes y carteles con QR

- Imagen 1200×630 por animal (foto, nombre, estado, logo), en su idioma y con los colores
  del tema; se regenera sola si cambia la foto o el nombre. Se usa en og:image/twitter
  (con Yoast o sin él) y se puede descargar desde la ficha del panel.
- Cartel A4 imprimible con QR a la ficha (acción de fila y acción en bloque).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-09-25 10:21:32 +02:00
parent c1bbf78b5c
commit 130c29bd61
5 changed files with 318 additions and 0 deletions

View File

@@ -0,0 +1,317 @@
<?php
/**
* Compartir: imagen automática para redes (1200×630) y carteles imprimibles con QR.
*/
defined( 'ABSPATH' ) || exit;
define( 'GA_SHARE_VERSION', 2 );
define( 'GA_QR_JS', 'https://cdn.jsdelivr.net/npm/qrcode-generator@2.0.4/dist/qrcode.js' );
define( 'GA_QR_SRI', 'sha384-e9EFD6BGC90bkW9aDV5xbbBfzwN7G8YImHao2lfLVKV/hPB0E0go+H3I64h7oHtA' );
/* -------------------------------------------------------------------------
* Imagen para redes
* ---------------------------------------------------------------------- */
/** Ruta y URL de la imagen de un animal (el nombre cambia si cambia la foto, el nombre o el diseño). */
function ga_share_image_paths( $post_id ) {
$uploads = wp_upload_dir();
$lang = function_exists( 'pll_get_post_language' ) ? pll_get_post_language( $post_id ) : 'es';
$hash = substr( md5( implode( '|', array(
GA_SHARE_VERSION, get_the_title( $post_id ), get_post_thumbnail_id( $post_id ), $lang, ga_status( $post_id ),
function_exists( 'ga_color' ) ? ga_color( 'primary' ) . ga_color( 'accent' ) : '',
) ) ), 0, 10 );
$name = "animal-$post_id-$hash.jpg";
return array(
'dir' => trailingslashit( $uploads['basedir'] ) . 'ga-share',
'file' => trailingslashit( $uploads['basedir'] ) . "ga-share/$name",
'url' => trailingslashit( $uploads['baseurl'] ) . "ga-share/$name",
'glob' => trailingslashit( $uploads['basedir'] ) . "ga-share/animal-$post_id-*.jpg",
);
}
/** Devuelve la URL de la imagen para redes, generándola si hace falta. */
function ga_share_image_url( $post_id ) {
$p = ga_share_image_paths( $post_id );
if ( ! file_exists( $p['file'] ) && ! ga_generate_share_image( $post_id ) ) {
return '';
}
return $p['url'];
}
function ga_hex_color( $img, $hex ) {
$hex = ltrim( $hex, '#' );
return imagecolorallocate( $img, hexdec( substr( $hex, 0, 2 ) ), hexdec( substr( $hex, 2, 2 ) ), hexdec( substr( $hex, 4, 2 ) ) );
}
/** Carga una imagen de la biblioteca (JPEG, PNG o WebP) como recurso GD. */
function ga_gd_load( $path ) {
$type = wp_check_filetype( $path )['type'] ?? '';
return match ( $type ) {
'image/jpeg' => @imagecreatefromjpeg( $path ),
'image/png' => @imagecreatefrompng( $path ),
'image/webp' => function_exists( 'imagecreatefromwebp' ) ? @imagecreatefromwebp( $path ) : false,
default => false,
};
}
/** Parte un texto en líneas que quepan en $width con la fuente y tamaño dados. */
function ga_gd_wrap( $text, $font, $size, $width ) {
$lines = array( '' );
foreach ( preg_split( '/\s+/u', $text ) as $word ) {
$try = trim( end( $lines ) . ' ' . $word );
$box = imagettfbbox( $size, 0, $font, $try );
if ( $box[2] - $box[0] > $width && '' !== end( $lines ) ) {
$lines[] = $word;
} else {
$lines[ count( $lines ) - 1 ] = $try;
}
}
return $lines;
}
function ga_generate_share_image( $post_id ) {
if ( ! function_exists( 'imagettftext' ) ) {
return false;
}
$p = ga_share_image_paths( $post_id );
wp_mkdir_p( $p['dir'] );
foreach ( glob( $p['glob'] ) ?: array() as $old ) {
@unlink( $old ); // Versiones anteriores de este animal.
}
$w = 1200;
$h = 630;
$split = 630; // Ancho de la foto (cuadrada, a la izquierda).
$img = imagecreatetruecolor( $w, $h );
$primary = function_exists( 'ga_color' ) ? ga_color( 'primary' ) : '#401f0c';
$accent = function_exists( 'ga_color' ) ? ga_color( 'accent' ) : '#bd8537';
$surface = function_exists( 'ga_color' ) ? ga_color( 'surface' ) : '#f3ecdc';
$bg = ga_hex_color( $img, $primary );
imagefilledrectangle( $img, 0, 0, $w, $h, $bg );
// Foto (recorte cuadrado centrado) o, si no hay, el símbolo del logo sobre fondo claro.
$photo = get_post_thumbnail_id( $post_id ) ? get_attached_file( get_post_thumbnail_id( $post_id ) ) : '';
$src = $photo ? ga_gd_load( $photo ) : false;
if ( $src ) {
$sw = imagesx( $src );
$sh = imagesy( $src );
$side = min( $sw, $sh );
imagecopyresampled( $img, $src, 0, 0, (int) ( ( $sw - $side ) / 2 ), (int) ( ( $sh - $side ) / 3 ), $split, $h, $side, $side );
imagedestroy( $src );
} else {
imagefilledrectangle( $img, 0, 0, $split, $h, ga_hex_color( $img, $surface ) );
$mark = @imagecreatefrompng( __DIR__ . '/assets/logo-mark.png' );
if ( $mark ) {
$mw = 380;
$mh = (int) ( imagesy( $mark ) * $mw / imagesx( $mark ) );
imagecopyresampled( $img, $mark, (int) ( ( $split - $mw ) / 2 ), (int) ( ( $h - $mh ) / 2 ), 0, 0, $mw, $mh, imagesx( $mark ), imagesy( $mark ) );
imagedestroy( $mark );
}
}
// Texto a la derecha.
$serif = __DIR__ . '/assets/fraunces-800.ttf';
$sans = __DIR__ . '/assets/karla-600.ttf';
$white = imagecolorallocate( $img, 255, 255, 255 );
$gold = ga_hex_color( $img, $accent );
$soft = ga_hex_color( $img, $surface );
$x = $split + 56;
$max = $w - $x - 50;
$lang = function_exists( 'pll_get_post_language' ) ? pll_get_post_language( $post_id ) : 'es';
$eu = 'eu' === $lang;
imagettftext( $img, 17, 0, $x, 92, $gold, $sans, $eu ? 'BILAGUN · ADOPZIOA' : 'BILAGUN · ADOPCIÓN' );
$name = get_the_title( $post_id );
$size = 64;
do {
$lines = ga_gd_wrap( $name, $serif, $size, $max );
$size -= 4;
} while ( ( count( $lines ) > 2 ) && $size > 30 );
$size += 4;
$y = 190;
foreach ( array_slice( $lines, 0, 3 ) as $line ) {
imagettftext( $img, $size, 0, $x, $y, $white, $serif, $line );
$y += (int) ( $size * 1.25 );
}
$status = ga_status( $post_id );
$tag = 'proceso' === $status ? ( $eu ? 'Prozesuan' : 'En proceso de adopción' ) : ( $eu ? 'Familia bila' : 'Busca familia' );
imagettftext( $img, 28, 0, $x, $y + 20, $soft, $sans, $tag );
$host = wp_parse_url( home_url(), PHP_URL_HOST );
imagettftext( $img, 18, 0, $x, $h - 60, $gold, $sans, $host );
$mark = @imagecreatefrompng( __DIR__ . '/assets/logo-mark.png' );
if ( $mark ) {
$mw = 96;
$mh = (int) ( imagesy( $mark ) * $mw / imagesx( $mark ) );
imagecopyresampled( $img, $mark, $w - $mw - 44, $h - $mh - 44, 0, 0, $mw, $mh, imagesx( $mark ), imagesy( $mark ) );
imagedestroy( $mark );
}
$ok = imagejpeg( $img, $p['file'], 86 );
imagedestroy( $img );
return $ok;
}
/* Se regenera al guardar (si cambió la foto o el nombre, cambia el nombre de archivo). */
add_action( 'save_post_animal', function ( $post_id ) {
if ( ! wp_is_post_revision( $post_id ) && 'publish' === get_post_status( $post_id ) ) {
ga_share_image_url( $post_id );
}
}, 30 );
/* Etiquetas Open Graph / Twitter con la imagen generada */
add_action( 'wpseo_add_opengraph_images', function ( $container ) {
if ( is_singular( 'animal' ) && ( $url = ga_share_image_url( get_queried_object_id() ) ) ) {
$container->add_image( array( 'url' => $url, 'width' => 1200, 'height' => 630, 'type' => 'image/jpeg' ) );
}
} );
add_filter( 'wpseo_opengraph_image', fn( $url ) => is_singular( 'animal' ) ? ( ga_share_image_url( get_queried_object_id() ) ?: $url ) : $url );
add_filter( 'wpseo_twitter_image', fn( $url ) => is_singular( 'animal' ) ? ( ga_share_image_url( get_queried_object_id() ) ?: $url ) : $url );
add_filter( 'wpseo_opengraph_image_width', fn( $v ) => is_singular( 'animal' ) ? 1200 : $v );
add_filter( 'wpseo_opengraph_image_height', fn( $v ) => is_singular( 'animal' ) ? 630 : $v );
add_filter( 'wpseo_twitter_card_type', fn( $v ) => is_singular( 'animal' ) ? 'summary_large_image' : $v );
/* Sin Yoast: etiquetas mínimas propias */
add_action( 'wp_head', function () {
if ( defined( 'WPSEO_VERSION' ) || ! is_singular( 'animal' ) ) {
return;
}
$id = get_queried_object_id();
printf( '<meta property="og:title" content="%s">' . "\n", esc_attr( get_the_title( $id ) ) );
printf( '<meta property="og:url" content="%s">' . "\n", esc_url( get_permalink( $id ) ) );
printf( '<meta property="og:image" content="%s">' . "\n", esc_url( ga_share_image_url( $id ) ) );
echo '<meta property="og:image:width" content="1200"><meta property="og:image:height" content="630"><meta name="twitter:card" content="summary_large_image">' . "\n";
}, 5 );
/* En la ficha del panel: vista previa y descarga (para publicarla en Instagram, etc.) */
add_action( 'add_meta_boxes_animal', function ( $post ) {
add_meta_box( 'ga_share', 'Compartir', function ( $post ) {
if ( 'publish' !== $post->post_status ) {
echo '<p class="description">Disponible cuando el animal esté publicado.</p>';
return;
}
$url = ga_share_image_url( $post->ID );
if ( $url ) {
printf( '<p><img src="%1$s" alt="" style="width:100%%;height:auto;border-radius:4px"></p><p><a class="button" href="%1$s" download>Descargar imagen para redes</a></p>', esc_url( $url ) );
}
printf(
'<p><a class="button" href="%s" target="_blank">Imprimir cartel con QR</a></p><p class="description">La imagen se crea sola con la foto principal y el nombre, y es la que aparece al compartir el enlace en WhatsApp o redes.</p>',
esc_url( ga_poster_url( array( $post->ID ) ) )
);
}, 'animal', 'side', 'default' );
} );
/* -------------------------------------------------------------------------
* Carteles con QR (para las jaulas, jornadas de adopción, etc.)
* ---------------------------------------------------------------------- */
function ga_poster_url( array $ids ) {
return wp_nonce_url( admin_url( 'admin-post.php?action=ga_poster&ids=' . implode( ',', array_map( 'absint', $ids ) ) ), 'ga_poster' );
}
add_filter( 'post_row_actions', function ( $actions, $post ) {
if ( 'animal' === $post->post_type && 'publish' === $post->post_status ) {
$actions['ga_poster'] = sprintf( '<a href="%s" target="_blank">Cartel QR</a>', esc_url( ga_poster_url( array( $post->ID ) ) ) );
}
return $actions;
}, 10, 2 );
add_filter( 'bulk_actions-edit-animal', function ( $actions ) {
$actions['ga_posters'] = 'Imprimir carteles con QR';
return $actions;
} );
add_filter( 'handle_bulk_actions-edit-animal', function ( $redirect, $action, $ids ) {
return 'ga_posters' === $action ? ga_poster_url( $ids ) : $redirect;
}, 10, 3 );
add_action( 'admin_post_ga_poster', function () {
check_admin_referer( 'ga_poster' );
if ( ! current_user_can( 'edit_posts' ) ) {
wp_die( 'Sin permiso.' );
}
$ids = array_filter( array_map( 'absint', explode( ',', (string) ( $_GET['ids'] ?? '' ) ) ) );
$ids = array_filter( $ids, fn( $id ) => 'animal' === get_post_type( $id ) && 'publish' === get_post_status( $id ) );
if ( ! $ids ) {
wp_die( 'No hay animales publicados seleccionados.' );
}
$primary = function_exists( 'ga_color' ) ? ga_color( 'primary' ) : '#401f0c';
$accent = function_exists( 'ga_color' ) ? ga_color( 'accent' ) : '#bd8537';
$logo = function_exists( 'ga_logo_svg' ) ? ga_logo_svg( 'full' ) : '';
$fonts = content_url( 'themes/gure-ametsa/assets/fonts/' );
?>
<!doctype html>
<html lang="es">
<head>
<meta charset="utf-8">
<title>Carteles · Bilagun</title>
<style>
@font-face { font-family: Fraunces; src: url("<?php echo esc_url( $fonts . 'fraunces.woff2' ); ?>") format("woff2"); font-weight: 100 900; }
@font-face { font-family: Karla; src: url("<?php echo esc_url( $fonts . 'karla.woff2' ); ?>") format("woff2"); font-weight: 100 900; }
@page { size: A4; margin: 0; }
* { box-sizing: border-box; }
body { margin: 0; font-family: Karla, sans-serif; color: <?php echo esc_html( $primary ); ?>; background: #eee; }
.bar { padding: 12px 20px; background: <?php echo esc_html( $primary ); ?>; color: #fff; display: flex; gap: 16px; align-items: center; }
.bar button { font: 700 15px Karla, sans-serif; padding: 8px 18px; border-radius: 999px; border: 0; background: <?php echo esc_html( $accent ); ?>; cursor: pointer; }
.poster { width: 210mm; height: 297mm; margin: 10mm auto; background: #fff; padding: 16mm; display: flex; flex-direction: column; page-break-after: always; }
.photo { flex: 1; min-height: 0; border-radius: 6mm; overflow: hidden; background: #f3ecdc; display: grid; place-items: center; }
.photo img { width: 100%; height: 100%; object-fit: cover; }
.photo svg { width: 60%; height: auto; }
h1 { font: 800 30mm/1 Fraunces, serif; margin: 9mm 0 3mm; letter-spacing: -0.02em; }
.lead { font-size: 6mm; margin: 0 0 6mm; }
.foot { display: flex; align-items: center; gap: 8mm; border-top: 0.6mm solid #e6d9b7; padding-top: 7mm; }
.qr svg { width: 48mm; height: 48mm; display: block; }
.cta { flex: 1; }
.cta strong { display: block; font: 800 9mm/1.1 Fraunces, serif; }
.cta span { display: block; font-size: 5mm; margin-top: 2mm; color: <?php echo esc_html( $accent ); ?>; word-break: break-all; }
.logo svg { width: 34mm; height: auto; }
@media print { body { background: #fff; } .bar { display: none; } .poster { margin: 0; } }
</style>
</head>
<body>
<div class="bar"><button onclick="window.print()">Imprimir</button> <?php echo count( $ids ); ?> cartel(es) · A4</div>
<?php foreach ( $ids as $id ) : ?>
<?php
$photo = get_the_post_thumbnail_url( $id, 'large' );
$lead = has_excerpt( $id ) ? get_the_excerpt( $id ) : '';
?>
<section class="poster">
<div class="photo">
<?php if ( $photo ) : ?>
<img src="<?php echo esc_url( $photo ); ?>" alt="">
<?php else : ?>
<?php echo function_exists( 'ga_logo_svg' ) ? ga_logo_svg( 'mark' ) : ''; ?>
<?php endif; ?>
</div>
<h1><?php echo esc_html( get_the_title( $id ) ); ?></h1>
<?php if ( $lead ) : ?>
<p class="lead"><?php echo esc_html( $lead ); ?></p>
<?php endif; ?>
<div class="foot">
<div class="qr" data-qr="<?php echo esc_url( get_permalink( $id ) ); ?>"></div>
<div class="cta">
<strong>¿Quieres conocerme?<br>Ezagutu nahi nauzu?</strong>
<span><?php echo esc_html( preg_replace( '#^https?://#', '', get_permalink( $id ) ) ); ?></span>
</div>
<div class="logo"><?php echo $logo; // SVG del tema. ?></div>
</div>
</section>
<?php endforeach; ?>
<script src="<?php echo esc_url( GA_QR_JS ); ?>" integrity="<?php echo esc_attr( GA_QR_SRI ); ?>" crossorigin="anonymous"></script>
<script>
document.querySelectorAll( '[data-qr]' ).forEach( function ( el ) {
var qr = qrcode( 0, 'M' );
qr.addData( el.dataset.qr );
qr.make();
el.innerHTML = qr.createSvgTag( { cellSize: 4, margin: 0, scalable: true } );
} );
</script>
</body>
</html>
<?php
exit;
} );