mode = self::MODE_ASYNC === $mode ? self::MODE_ASYNC : self::MODE_SYNC; $this->delay = max( 0, $delay ); $this->fail_with = $fail_with; $this->sleep = $sleep ?? 'sleep'; $this->clock = $clock ?? 'time'; } /** * Provider ID. */ public function id(): string { return 'mock'; } /** * Starts a fake generation. * * @param TryOnInput $input Photo, product images and prompt. */ public function submit( TryOnInput $input ): Submission { if ( null !== $this->fail_with ) { return Submission::failed( new ProviderError( $this->fail_with ) ); } if ( self::MODE_ASYNC === $this->mode ) { $ready_at = ( $this->clock )() + $this->delay; return Submission::pending( sprintf( 'mock-%010d-%s', $ready_at, bin2hex( random_bytes( 4 ) ) ) ); } if ( $this->delay > 0 ) { ( $this->sleep )( $this->delay ); } $image = $this->compose( $input ); return null === $image ? Submission::failed( new ProviderError( ProviderError::BAD_REQUEST ) ) : Submission::done( $image, self::MODEL, '0.000000' ); } /** * Checks a fake async generation. * * @param string $remote_id ID from submit(). */ public function poll( string $remote_id ): Submission { if ( 1 !== preg_match( self::JOB_ID, $remote_id, $match ) ) { return Submission::failed( new ProviderError( ProviderError::BAD_REQUEST ) ); } if ( ( $this->clock )() < (int) $match[1] ) { return Submission::pending( $remote_id ); } $image = $this->placeholder(); return null === $image ? Submission::failed( new ProviderError( ProviderError::NO_IMAGE ) ) : Submission::done( $image, self::MODEL, '0.000000' ); } /** * The shopper's photo with the first product image in a corner and a banner. * * @param TryOnInput $input Input. */ private function compose( TryOnInput $input ): ?string { $photo = @imagecreatefromstring( $input->person() ); // phpcs:ignore WordPress.PHP.NoSilencedErrors.Discouraged -- GD warns on bad data; false is handled. if ( false === $photo ) { return null; } $width = imagesx( $photo ); $height = imagesy( $photo ); $references = $input->references(); if ( array() !== $references ) { $product = @imagecreatefromstring( $references[0]->bytes() ); // phpcs:ignore WordPress.PHP.NoSilencedErrors.Discouraged -- GD warns on bad data; false is handled. if ( false !== $product ) { $side = (int) round( min( $width, $height ) * 0.3 ); $margin = (int) round( $side * 0.08 ); $x = $width - $side - 2 * $margin; $y = $height - $side - 2 * $margin; imagefilledrectangle( $photo, $x - $margin, $y - $margin, $x + $side + $margin, $y + $side + $margin, (int) imagecolorallocate( $photo, 255, 255, 255 ) ); imagecopyresampled( $photo, $product, $x, $y, 0, 0, $side, $side, imagesx( $product ), imagesy( $product ) ); } } $this->banner( $photo, $width ); return $this->encode( $photo ); } /** * A neutral portrait placeholder for async results. */ private function placeholder(): ?string { $image = imagecreatetruecolor( 1024, 1536 ); if ( false === $image ) { return null; } imagefill( $image, 0, 0, (int) imagecolorallocate( $image, 225, 225, 225 ) ); $this->banner( $image, 1024 ); return $this->encode( $image ); } /** * Draws the "MOCK PREVIEW" banner across the top. * * @param \GdImage|resource $image Image. * @param int $width Image width. */ private function banner( $image, int $width ): void { imagefilledrectangle( $image, 0, 0, $width, 28, (int) imagecolorallocate( $image, 20, 20, 20 ) ); imagestring( $image, 5, 10, 6, 'MOCK PREVIEW - not a real try-on', (int) imagecolorallocate( $image, 255, 255, 255 ) ); } /** * Encodes as JPEG. * * @param \GdImage|resource $image Image. */ private function encode( $image ): ?string { ob_start(); $ok = imagejpeg( $image, null, self::QUALITY ); $bytes = (string) ob_get_clean(); return $ok && '' !== $bytes ? $bytes : null; } }