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 );
}
}