Skip to main content
Philo provides seamless image handling with automatic asset management, local storage, and markdown-based referencing.

Adding Images

Drag and Drop

Drag image files directly into the editor:
FileHandler.configure({
  allowedMimeTypes: ["image/png", "image/jpeg", "image/gif", "image/webp"],
  onDrop: (_editor: TiptapEditor, files: File[], pos: number) => {
    (async () => {
      for (const file of files) {
        const relativePath = await saveImage(file);
        const assetUrl = await resolveAssetUrl(relativePath);
        _editor.chain().insertContentAt(pos, {
          type: "image",
          attrs: { src: assetUrl, alt: file.name },
        }).focus().run();
      }
    })().catch(console.error);
    return true;
  },
})

Paste from Clipboard

Paste images directly from clipboard:
onPaste: (_editor: TiptapEditor, files: File[]) => {
  (async () => {
    for (const file of files) {
      const relativePath = await saveImage(file);
      const assetUrl = await resolveAssetUrl(relativePath);
      _editor.chain().focus().insertContent({
        type: "image",
        attrs: { src: assetUrl, alt: file.name },
      }).run();
    }
  })().catch(console.error);
  return true;
}

Supported Formats

PNG, JPEG, GIF, WebP

Automatic Storage

Files saved to assets directory with unique names

Image Storage

Images are saved with timestamped, unique filenames:
let imageIndex = 0;

function generateFilename(ext: string): string {
  const ts = Date.now();
  const index = imageIndex++;
  return `image_${ts}_${index}.${ext.toLowerCase()}`;
}

export async function saveImage(file: File): Promise<string> {
  const assetsDir = await getAssetsDir();
  const dirExists = await exists(assetsDir);
  if (!dirExists) {
    await mkdir(assetsDir, { recursive: true });
  }

  const ext = file.name.split(".").pop() || "png";
  const filename = generateFilename(ext);
  const fullPath = await join(assetsDir, filename);

  const arrayBuffer = await file.arrayBuffer();
  const uint8Array = new Uint8Array(arrayBuffer);

  await writeFile(fullPath, uint8Array);

  return filename;
}

Filename Examples

image_1709552400000_0.png
image_1709552401234_1.jpg
image_1709552402567_2.webp
The index counter prevents collisions even within the same millisecond.

Asset Directory

Images are stored in a configurable assets directory:
async function getAssetsRelativeRoot(): Promise<string> {
  const configured = normalizePathSegment(await getAssetsFolderSetting());
  return configured || "assets";
}
Default location:
  • Relative to journal: {journal}/assets/
  • Configurable via settings: Custom path option

URL Resolution

Images stored as relative paths are resolved to Tauri asset URLs:
export async function resolveAssetUrl(relativePath: string): Promise<string> {
  const absolutePath = await resolveAssetAbsolutePath(relativePath);
  return convertFileSrc(absolutePath);
}

async function resolveAssetAbsolutePath(relativePath: string): Promise<string> {
  const assetsRelativeRoot = await getAssetsRelativeRoot();
  const assetSuffix = getAssetSuffix(relativePath, assetsRelativeRoot);
  
  if (assetSuffix !== null) {
    const assetsDir = await getAssetsDir();
    return assetSuffix ? await join(assetsDir, assetSuffix) : assetsDir;
  }

  const journalDir = await getJournalDir();
  return await join(journalDir, normalizeRelativeAssetPath(relativePath));
}

Asset URL Format

Tauri’s convertFileSrc() produces URLs like:
http://asset.localhost/path/to/image.png
or
asset://localhost/path/to/image.png
Do not use base64 encoding for images. Philo uses asset URLs for better performance and storage efficiency.

Markdown Image Handling

Resolving Images in Markdown

When loading markdown content, relative paths are resolved to asset URLs:
export async function resolveMarkdownImages(markdown: string): Promise<string> {
  const imagePattern = /!\[([^\]]*)\]\(([^)\s"]+)(?:\s+"([^"]*)")?\)/g;
  const matches = [...markdown.matchAll(imagePattern)];
  if (matches.length === 0) return markdown;

  let result = markdown;

  for (const match of matches) {
    const [full, alt, path, title] = match;
    if (isNonAssetUrl(path)) continue;

    const relativePath = normalizeRelativeAssetPath(path);
    const absolutePath = await resolveAssetAbsolutePath(relativePath);
    if (!(await exists(absolutePath))) continue;

    const assetUrl = convertFileSrc(absolutePath);
    const replacement = title
      ? `![${alt}](${assetUrl} "${title}")`
      : `![${alt}](${assetUrl})`;
    result = result.replace(full, replacement);
  }

  return result;
}

Unresolving Images in Markdown

When saving, asset URLs are converted back to relative paths:
export function unresolveMarkdownImages(markdown: string): string {
  const assetUrlPattern =
    /!\[([^\]]*)\]\(((?:http:\/\/asset\.localhost|asset:\/\/localhost)[^)\s"]+)(?:\s+"([^"]*)")?\)/g;

  return markdown.replace(assetUrlPattern, (_full, alt, url, title) => {
    let filename = "";
    try {
      const parsed = new URL(url);
      const segments = decodeURIComponent(parsed.pathname).split("/").filter(Boolean);
      filename = segments[segments.length - 1] || "";
    } catch {
      const match = String(url).match(/\/([^/]+)$/);
      filename = match ? match[1] : "";
    }
    if (!filename) {
      return title
        ? `![${alt}](${url} "${title}")`
        : `![${alt}](${url})`;
    }
    return title
      ? `![${alt}](${filename} "${title}")`
      : `![${alt}](${filename})`;
  });
}
Storing relative paths in markdown makes notes portable across different systems.

Path Normalization

Various path formats are normalized to consistent relative paths:
function normalizeRelativeAssetPath(path: string): string {
  return path.replace(/^(?:\.\.\/)+/, "");
}

function normalizePathSegment(segment: string): string {
  return segment.replace(/^\.?\//, "").replace(/\/$/, "");
}

function getAssetSuffix(
  relativePath: string,
  assetsRelativeRoot: string,
): string | null {
  const normalized = normalizeRelativeAssetPath(relativePath);
  if (!normalized) return null;
  
  const prefix = `${assetsRelativeRoot}/`;
  if (normalized === assetsRelativeRoot) {
    return "";
  }
  if (normalized.startsWith(prefix)) {
    return normalized.slice(prefix.length);
  }
  if (!normalized.includes("/")) {
    return normalized;
  }
  return null;
}

Supported Path Formats

![Alt text](image_123_0.png)
![Alt text](assets/image_123_0.png)
![Alt text](./assets/image_123_0.png)
![Alt text](../assets/image_123_0.png)
All resolve to the same asset file.

URL Detection

External URLs and data URIs are not processed:
function isNonAssetUrl(path: string): boolean {
  return /^(?:[a-z]+:)?\/\//i.test(path)
    || /^[a-z]+:/i.test(path)
    || path.startsWith("/")
    || path.startsWith("#");
}
Examples of non-asset URLs:
![External](https://example.com/image.png)
![Data](data:image/png;base64,...)
![Absolute](/absolute/path/image.png)
These are left as-is during resolution.

Editor Configuration

The Tiptap Image extension is configured for inline images:
Image.configure({ 
  inline: true, 
  allowBase64: false 
})
allowBase64: false prevents base64 encoding, ensuring all images use the asset system.

Best Practices

1

Use drag-and-drop

Fastest way to add images—just drag from file manager into editor
2

Optimize before adding

Compress large images to save storage space
3

Add alt text

Provide descriptive alt text for accessibility
4

Backup assets folder

Include the assets directory when backing up your journal

Troubleshooting

  • Verify the file exists in the assets directory
  • Check that the filename in markdown matches the saved file
  • Ensure the file format is supported (PNG, JPEG, GIF, WebP)
  • Try resolving the asset URL manually with resolveAssetUrl()
  • Confirm FileHandler extension is configured
  • Check browser console for errors
  • Verify file MIME type is in allowedMimeTypes
  • Philo creates it automatically on first image save
  • Check filesystem permissions
  • Verify journal directory is properly configured
  • Ensure you moved the entire assets directory
  • Check that assets folder setting is correct
  • Try re-resolving images with resolveMarkdownImages()

Performance Considerations

No base64

Asset URLs load faster than base64-encoded images

Lazy loading

Images only load when visible in viewport

Unique filenames

Prevents caching issues and filename collisions

Local storage

No network requests—all images stored locally
Deleting images from the assets folder will break references in journal entries. Philo does not currently track image usage.

Future Enhancements

  • Image compression: Automatic optimization on save
  • Usage tracking: Identify and clean up unused images
  • Gallery view: Browse all images across journal
  • Image editing: Basic cropping and rotation
  • Cloud sync: Optional cloud storage for assets

Build docs developers (and LLMs) love