Skip to content

Commit

Permalink
Implement several helper API methods related to dealing with binary f…
Browse files Browse the repository at this point in the history
…iles, blobs, and data URLs.
  • Loading branch information
felixarntz committed Feb 19, 2025
1 parent aed69c1 commit a05d685
Show file tree
Hide file tree
Showing 5 changed files with 243 additions and 22 deletions.
2 changes: 1 addition & 1 deletion examples/add-image-alt-text-plugin/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ function ImageControls( { attributes, setAttributes } ) {
setInProgress( true );

const mimeType = getMimeType( attributes.url );
const base64Image = await helpers.base64EncodeFile( attributes.url );
const base64Image = await helpers.fileToBase64DataUrl( attributes.url );

let candidates;
try {
Expand Down
73 changes: 63 additions & 10 deletions includes/Services/API/Helpers.php
Original file line number Diff line number Diff line change
Expand Up @@ -9,12 +9,14 @@
namespace Felix_Arntz\AI_Services\Services\API;

use Felix_Arntz\AI_Services\Services\API\Enums\Content_Role;
use Felix_Arntz\AI_Services\Services\API\Types\Blob;
use Felix_Arntz\AI_Services\Services\API\Types\Candidates;
use Felix_Arntz\AI_Services\Services\API\Types\Content;
use Felix_Arntz\AI_Services\Services\API\Types\Parts;
use Felix_Arntz\AI_Services\Services\API\Types\Parts\Text_Part;
use Felix_Arntz\AI_Services\Services\Util\Formatter;
use Generator;
use InvalidArgumentException;
use WP_Post;

/**
Expand Down Expand Up @@ -82,7 +84,7 @@ public static function text_and_attachment_to_content( string $text, $attachment

$parts = new Parts();
$parts->add_text_part( $text );
$parts->add_inline_data_part( $mime_type, self::base64_encode_file( $file, $mime_type ) );
$parts->add_inline_data_part( $mime_type, self::file_to_base64_data_url( $file, $mime_type ) );

return Formatter::format_content( $parts, $role );
}
Expand Down Expand Up @@ -203,7 +205,7 @@ public static function process_candidates_stream( Generator $generator ): Candid
}

/**
* Base64-encodes a file and returns its data URL.
* Returns the base64-encoded data URL representation of the given file URL.
*
* @since n.e.x.t
*
Expand All @@ -212,18 +214,69 @@ public static function process_candidates_stream( Generator $generator ): Candid
* be prefixed with `data:{mime_type};base64,`. Default empty string.
* @return string The base64-encoded file data URL, or empty string on failure.
*/
public static function base64_encode_file( string $file, string $mime_type = '' ): string {
// phpcs:ignore WordPress.WP.AlternativeFunctions.file_get_contents_file_get_contents
$binary_data = file_get_contents( $file );
if ( ! $binary_data ) {
public static function file_to_base64_data_url( string $file, string $mime_type = '' ): string {
$blob = self::file_to_blob( $file, $mime_type );
if ( ! $blob ) {
return '';
}

return self::blob_to_base64_data_url( $blob );
}

/**
* Returns the binary data blob representation of the given file URL.
*
* @since n.e.x.t
*
* @param string $file Absolute path to the file, or its URL.
* @param string $mime_type Optional. The MIME type of the file. If provided, the automatically detected MIME type
* will be overwritten. Default empty string.
* @return Blob|null The binary data blob, or null on failure.
*/
public static function file_to_blob( string $file, string $mime_type = '' ): ?Blob {
try {
return Blob::from_file( $file, $mime_type );
} catch ( InvalidArgumentException $e ) {
return null;
}
}

/**
* Returns the base64-encoded data URL representation of the given binary data blob.
*
* @since n.e.x.t
*
* @param Blob $blob The binary data blob.
* @return string The base64-encoded file data URL, or empty string on failure.
*/
public static function blob_to_base64_data_url( Blob $blob ): string {
// phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.obfuscation_base64_encode
$base64 = base64_encode( $binary_data );
if ( '' !== $mime_type ) {
$base64 = "data:$mime_type;base64,$base64";
$base64 = base64_encode( $blob->get_binary_data() );
$mime_type = $blob->get_mime_type();
return "data:$mime_type;base64,$base64";
}

/**
* Returns the binary data blob representation of the given base64-encoded data URL.
*
* @since n.e.x.t
*
* @param string $base64_data_url The base64-encoded data URL.
* @return Blob|null The binary data blob, or null on failure.
*/
public static function base64_data_url_to_blob( string $base64_data_url ): ?Blob {
if ( ! preg_match( '/^data:([a-z0-9-]+\/[a-z0-9-]+);base64,/', $base64_data_url, $matches ) ) {
return null;
}
return $base64;

$base64 = substr( $base64_data_url, strlen( $matches[0] ) );

// phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.obfuscation_base64_decode
$binary_data = base64_decode( $base64 );
if ( false === $binary_data ) {
return null;
}

return new Blob( $binary_data, $matches[1] );
}
}
103 changes: 103 additions & 0 deletions includes/Services/API/Types/Blob.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
<?php
/**
* Class Felix_Arntz\AI_Services\Services\API\Types\Blob
*
* @since n.e.x.t
* @package ai-services
*/

namespace Felix_Arntz\AI_Services\Services\API\Types;

use InvalidArgumentException;

/**
* Simple value class representing a binary data blob, e.g. from a file.
*
* @since n.e.x.t
*/
final class Blob {

/**
* The binary data of the blob.
*
* @since n.e.x.t
* @var string
*/
private $binary_data;

/**
* The MIME type of the blob.
*
* @since n.e.x.t
* @var string
*/
private $mime_type;

/**
* Constructor.
*
* @since n.e.x.t
*
* @param string $binary_data The binary data of the blob.
* @param string $mime_type The MIME type of the blob.
*/
public function __construct( string $binary_data, string $mime_type ) {
$this->binary_data = $binary_data;
$this->mime_type = $mime_type;
}

/**
* Retrieves the binary data of the blob.
*
* @since n.e.x.t
*
* @return string The binary data.
*/
public function get_binary_data(): string {
return $this->binary_data;
}

/**
* Retrieves the MIME type of the blob.
*
* @since n.e.x.t
*
* @return string The MIME type.
*/
public function get_mime_type(): string {
return $this->mime_type;
}

/**
* Creates a new blob instance from a file.
*
* @since n.e.x.t
*
* @param string $file The file path or URL.
* @param string $mime_type Optional. MIME type, to override the automatic detection. Default empty string.
* @return Blob The blob instance.
*
* @throws InvalidArgumentException Thrown if the file could not be read or if the MIME type cannot be determined.
*/
public static function from_file( string $file, string $mime_type = '' ): self {
// phpcs:ignore WordPress.WP.AlternativeFunctions.file_get_contents_file_get_contents
$blob = file_get_contents( $file );
if ( ! $blob ) {
throw new InvalidArgumentException(
sprintf( 'Could not read file %s.', esc_html( $file ) )
);
}

if ( ! $mime_type ) {
$file_type = wp_check_filetype( $file );
if ( ! $file_type['type'] ) {
throw new InvalidArgumentException(
sprintf( 'Could not determine MIME type of file %s.', esc_html( $file ) )
);
}
$mime_type = $file_type['type'];
}

return new self( $blob, $mime_type );
}
}
85 changes: 75 additions & 10 deletions src/ai/helpers.js
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ export async function textAndAttachmentToContent(
role = ContentRole.USER
) {
const mimeType = attachment.mime;
const data = await base64EncodeFile(
const data = await fileToBase64DataUrl(
attachment.sizes?.large?.url || attachment.url
);

Expand Down Expand Up @@ -162,7 +162,7 @@ export function processCandidatesStream( generator ) {
}

/**
* Base64-encodes a file and returns its data URL.
* Returns the base64-encoded data URL representation of the given file URL.
*
* @since n.e.x.t
*
Expand All @@ -171,11 +171,47 @@ export function processCandidatesStream( generator ) {
* be prefixed with `data:{mime_type};base64,`. Default empty string.
* @return {string} The base64-encoded file data URL, or empty string on failure.
*/
export async function base64EncodeFile( file, mimeType = '' ) {
export async function fileToBase64DataUrl( file, mimeType = '' ) {
const blob = await fileToBlob( file, mimeType );
if ( ! blob ) {
return '';
}

return blobToBase64DataUrl( blob );
}

/**
* Returns the binary data blob representation of the given file URL.
*
* @since n.e.x.t
*
* @param {string} file The file URL.
* @param {string} mimeType Optional. The MIME type of the file. If provided, the automatically detected MIME type will
* be overwritten. Default empty string.
* @return {Blob?} The binary data blob, or null on failure.
*/
export async function fileToBlob( file, mimeType = '' ) {
const data = await fetch( file );
const blob = await data.blob();
if ( ! blob ) {
return null;
}
if ( mimeType && mimeType !== blob.type ) {
return new Blob( [ blob ], { type: mimeType } );
}
return blob;
}

const base64 = await new Promise( ( resolve ) => {
/**
* Returns the base64-encoded data URL representation of the given binary data blob.
*
* @since n.e.x.t
*
* @param {Blob} blob The binary data blob.
* @return {string} The base64-encoded data URL, or empty string on failure.
*/
export async function blobToBase64DataUrl( blob ) {
const base64DataUrl = await new Promise( ( resolve ) => {
const reader = new window.FileReader();
reader.readAsDataURL( blob );
reader.onloadend = () => {
Expand All @@ -184,11 +220,40 @@ export async function base64EncodeFile( file, mimeType = '' ) {
};
} );

if ( mimeType ) {
return base64.replace(
/^data:[a-z0-9-]+\/[a-z0-9-]+;base64,/,
`data:${ mimeType };base64,`
);
return base64DataUrl;
}

/**
* Returns the binary data blob representation of the given base64-encoded data URL.
*
* @since n.e.x.t
*
* @param {string} base64DataUrl The base64-encoded data URL.
* @return {Blob?} The binary data blob, or null on failure.
*/
export async function base64DataUrlToBlob( base64DataUrl ) {
const prefixMatch = base64DataUrl.match(
/^data:([a-z0-9-]+\/[a-z0-9-]+);base64,/
);
if ( ! prefixMatch ) {
return null;
}

const base64Data = base64DataUrl.substring( prefixMatch[ 0 ].length );
const binaryData = atob( base64Data );
const byteArrays = [];

for ( let offset = 0; offset < binaryData.length; offset += 512 ) {
const slice = binaryData.slice( offset, offset + 512 );

const byteNumbers = new Array( slice.length );
for ( let i = 0; i < slice.length; i++ ) {
byteNumbers[ i ] = slice.charCodeAt( i );
}
byteArrays.push( new Uint8Array( byteNumbers ) );
}
return base64;

return new Blob( byteArrays, {
type: prefixMatch[ 1 ],
} );
}
2 changes: 1 addition & 1 deletion src/playground-page/store/messages.js
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ const parseContentFromCache = async ( content, attachment ) => {
...part,
inlineData: {
...part.inlineData,
data: await helpers.base64EncodeFile(
data: await helpers.fileToBase64DataUrl(
attachment.sizes?.large?.url || attachment.url
),
},
Expand Down

0 comments on commit a05d685

Please sign in to comment.