Request-cache voor de hard-crop-controle. */ private array $crop_cache = array(); /** Of er een redacteur meekijkt. Eén keer bepalen per request. */ private ?bool $editor = null; /** Of dit verzoek in de vergelijkingsstand staat. */ private ?bool $comparing = null; private ?bool $edit_mode = null; private Journal $journal; /** * @param FocusRepository $repository De repository. */ public function __construct( FocusRepository $repository ) { $this->repository = $repository; $this->journal = new Journal(); } /** * Het journaal van dit request. * * @return Journal */ public function journal(): Journal { return $this->journal; } /** * De ingestelde stand. * * @return string Een van de MODE_-constanten. */ public function mode(): string { // De vergelijkingsstand: `?yf-off=1` schakelt de plugin uit voor dit ene // verzoek, zodat je twee tabbladen naast elkaar kunt leggen en met eigen // ogen ziet wat het focuspunt doet. Alleen voor wie mag uploaden — een // bezoeker kan de site hiermee niet beïnvloeden. if ( $this->comparing() ) { return self::MODE_OFF; } $mode = (string) get_option( self::OPTION, self::MODE_AUTO ); if ( ! in_array( $mode, self::modes(), true ) ) { $mode = self::MODE_AUTO; } /** * Filtert de toepassingsstand. * * Hiermee kan een thema of plugin de plugin per pagina, per posttype of * per template uitschakelen zonder de instelling aan te raken. * * @param string $mode De stand. */ return (string) apply_filters( 'yinga_focus/apply_mode', $mode ); } /** * Alle geldige standen. * * @return string[] */ public static function modes(): array { return array( self::MODE_AUTO, self::MODE_PROPERTY, self::MODE_OFF ); } /** * Moeten we deze afbeelding aanraken? * * @param int $attachment_id Attachment-ID. * @param string|int[]|null $size Het opgevraagde beeldformaat. * @return bool */ public function should_apply( int $attachment_id, $size = null ): bool { if ( self::MODE_OFF === $this->mode() ) { $this->journal->record( $attachment_id, Journal::OFF ); return false; } if ( is_admin() && ! wp_doing_ajax() ) { return false; } if ( ! $this->repository->has( $attachment_id ) ) { $this->journal->record( $attachment_id, Journal::NO_DATA ); return false; } if ( null !== $size && $this->is_hard_cropped( $attachment_id, $size ) ) { // Niet toepassen — maar wélke boodschap dat verdient hangt ervan af // of FocalCrops het bestand al rond het focuspunt heeft gesneden. $this->journal->record( $attachment_id, $this->hard_crop_verdict( $attachment_id ) ); return false; } /** * Laatste woord over één afbeelding. * * @param bool $apply Of we toepassen. * @param int $attachment_id Attachment-ID. * @param string|int[]|null $size Het beeldformaat. */ $apply = (bool) apply_filters( 'yinga_focus/should_apply', true, $attachment_id, $size ); if ( ! $apply ) { $this->journal->record( $attachment_id, Journal::FILTERED ); } return $apply; } /** * De attributen die op een `` moeten komen. * * Altijd de custom property én de class; `object-position` alleen in de * automatische stand. Zo kan een thema in de property-stand zelf beslissen * met `object-position: var(--yf-pos)`. * * Voor een ingelogde redacteur komt er één attribuut bij: het attachment-ID. * Dat kost een bezoeker niets — hij krijgt het niet — en het is precies wat * een editor op de live site nodig heeft om te weten welke afbeelding hij * onder de muis heeft. Zonder dit zou zo'n editor moeten gokken op de * bestandsnaam in de `src`. * * @param int $attachment_id Attachment-ID. * @param bool $allow_position Of `object-position` mee mag. Zet dit op false * als de afbeelding er al één heeft: dan heeft * iemand hem met de hand gezet en die wint. * @return array Leeg als er niets toe te passen valt. */ public function attributes( int $attachment_id, bool $allow_position = true ): array { return $this->build( $attachment_id, 'object-position', $allow_position, $allow_position ? Journal::APPLIED : Journal::PARTIAL ); } /** * De kale CSS-waarde, zonder eigenschap eromheen. * * Voor integraties die hun eigen stylesheet schrijven en dus geen * style-attribuut nodig hebben — Elementor, bijvoorbeeld. * * @param int $attachment_id Attachment-ID. * @return string|null Bijvoorbeeld "62% 28%". */ public function position( int $attachment_id ): ?string { return $this->repository->css_position( $attachment_id ); } /** * Hetzelfde, maar voor een element met een CSS-achtergrond. * * `background-size: cover` gedraagt zich identiek aan `object-fit: cover`, en * `background-position` speelt daar dezelfde rol als `object-position`. De * wiskunde is letterlijk dezelfde; alleen de eigenschapsnaam verschilt. * * @param int $attachment_id Attachment-ID. * @param bool $allow_position Of `background-position` mee mag. * @return array */ public function background_attributes( int $attachment_id, bool $allow_position = true ): array { return $this->build( $attachment_id, 'background-position', $allow_position, $allow_position ? Journal::BG_APPLIED : Journal::PARTIAL ); } /** * @param int $attachment_id Attachment-ID. * @param string $property De CSS-eigenschap. * @param bool $allow_position Of de eigenschap zelf mee mag. * @param string $verdict Wat we in het journaal zetten. * @return array */ private function build( int $attachment_id, string $property, bool $allow_position, string $verdict ): array { $position = $this->repository->css_position( $attachment_id ); if ( null === $position ) { return array(); } $style = Naming::css_var( 'pos' ) . ':' . $position . ';'; if ( $allow_position && self::MODE_AUTO === $this->mode() ) { $style .= $property . ':' . $position . ';'; } $attributes = array( 'class' => Naming::css_class( 'image' ), 'style' => $style, ); if ( $this->editor_present() ) { $attributes['data-yf-id'] = (string) $attachment_id; } $this->journal->record( $attachment_id, $verdict, $position ); return $attributes; } /** * Kijkt er iemand mee die dit mag wijzigen? * * De uitkomst geldt voor het hele request, dus we bepalen hem één keer. Voor * een uitgelogde bezoeker — het overgrote deel van het verkeer — is dat één * goedkope controle en verder niets. * * @return bool */ private function editor_present(): bool { if ( null === $this->editor ) { $this->editor = is_user_logged_in() && current_user_can( 'upload_files' ); if ( $this->editor ) { $this->journal->activate(); } } return $this->editor; } /** * Staat deze pagina in de vergelijkingsstand? * * @return bool */ public function comparing(): bool { if ( null === $this->comparing ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- leesactie zonder bijwerking, afgeschermd op capability. $this->comparing = isset( $_GET[ self::COMPARE_ARG ] ) && $this->editor_present(); } return $this->comparing; } /** * Wijkt de verhouding van een geserveerd bestand af van het origineel? * * Voor afbeeldingen waarvan we alleen de bestandsnaam hebben — een `` * die het thema met de hand schrijft. Uit `foto-1400x1050.jpg` kennen we de * geserveerde maat; wijkt die verhouding af van het origineel, dan is het * bestand hard bijgesneden en is ons punt daar betekenisloos. * * @param int $attachment_id Attachment-ID. * @param float $width Geserveerde breedte. * @param float $height Geserveerde hoogte. * @return bool True bij afwijking, of als we het origineel niet kennen. */ public function ratio_differs( int $attachment_id, float $width, float $height ): bool { if ( $width <= 0 || $height <= 0 ) { return true; } $original = $this->repository->dimensions( $attachment_id ); if ( null === $original || $original['height'] <= 0 ) { return true; } $original_ratio = $original['width'] / $original['height']; $served_ratio = $width / $height; return abs( $served_ratio - $original_ratio ) / $original_ratio > self::RATIO_TOLERANCE; } /** * Het juiste oordeel over een hard gecropt bestand. * * @param int $attachment_id Attachment-ID. * @return string Journal::FILE_CROP als het bestand rond het focuspunt is * gesneden, anders Journal::HARD_CROP. */ public function hard_crop_verdict( int $attachment_id ): string { // FocalCrops zit in de Pro-plugin; zonder Pro bestaat er geen rond het // focuspunt gesneden bestand en is het antwoord per definitie HARD_CROP. return class_exists( \Yinga\Focus\Media\FocalCrops::class ) && \Yinga\Focus\Media\FocalCrops::is_cropped( $attachment_id ) ? Journal::FILE_CROP : Journal::HARD_CROP; } /** * Staat de bewerkmodus op de voorkant aan? * * De bewerkmodus zelf zit in de Pro-plugin; die meldt zich via dit filter. * Zonder Pro is het antwoord altijd nee, zonder dat de kern ook maar één * klasse van Pro hoeft te kennen. * * @return bool */ public function edit_mode(): bool { if ( null === $this->edit_mode ) { /** * Meldt of de bewerkmodus op de voorkant actief is. * * @param bool $active Standaard false. */ $this->edit_mode = (bool) apply_filters( 'yinga_focus/edit_mode', false ); } return $this->edit_mode; } /** * Vergeet wie er meekijkt. * * Nodig in tests en in WP-CLI-loops die van gebruiker wisselen; in een * gewoon request verandert dat niet halverwege. */ public function flush_viewer(): void { $this->editor = null; $this->comparing = null; $this->edit_mode = null; } /** * Is het opgevraagde beeldformaat hard-cropped ten opzichte van het origineel? * * @param int $attachment_id Attachment-ID. * @param string|int[] $size Het beeldformaat. * @return bool True als de verhouding afwijkt, of als we het niet kunnen bepalen. */ public function is_hard_cropped( int $attachment_id, $size ): bool { $key = $attachment_id . '|' . ( is_array( $size ) ? implode( 'x', $size ) : (string) $size ); if ( isset( $this->crop_cache[ $key ] ) ) { return $this->crop_cache[ $key ]; } $original = $this->repository->dimensions( $attachment_id ); if ( null === $original || $original['height'] <= 0 ) { // Zonder afmetingen kunnen we niets vergelijken. Niet toepassen. return $this->crop_cache[ $key ] = true; } $served = wp_get_attachment_image_src( $attachment_id, $size ); if ( ! is_array( $served ) || empty( $served[1] ) || empty( $served[2] ) ) { // Onbekend formaat: aannemen dat het het origineel is. return $this->crop_cache[ $key ] = false; } $original_ratio = $original['width'] / $original['height']; $served_ratio = (float) $served[1] / (float) $served[2]; $difference = abs( $served_ratio - $original_ratio ) / $original_ratio; return $this->crop_cache[ $key ] = ( $difference > self::RATIO_TOLERANCE ); } }