Tag: image-processing

  • A Trio of Pibooth Plugins

    A Trio of Pibooth Plugins

    This past weekend I did a thing:

    One of the things that I had done, as previously blargged about here, is having a Pibooth Photo Booth.

    I ended up writing three custom plugins that I’m going to share here. I had plenty of help from ChatGPT because I hate Python and it’s the worst language invented and it should be destroyed.

    I also ended up hacking a bit of Pibooth core because its hook/plugin system was a bit lacking for my needs, but that was only around moving around some of the touchscreen UI items for a better user experience. But enough about that, here’s my three plugins:

    Fullscreen Toggle

    One of the things I wanted to be able to easily do, running a keyboard/mouseless touchscreen Raspbery Pi was to be able to jump in and out of Fullscreen mode to do things like debug printers, change Wifi, etc. So I pulled this plugin out of the ether. It allows you to touch the top right corner (or click, if that’s your thing) to toggle between fullscreen and windowed mode.

    import pygame
    from pibooth import hookimpl
    from pibooth.utils import LOGGER
    
    __version__ = "0.0.1"
    
    """
    Plugin to toggle fullscreen/windowed mode by tapping top right corner in take-a-picture screen.
    """
    
    # Fractional region size
    REGION_WIDTH_FRAC = 0.10  # 10% width
    REGION_HEIGHT_FRAC = 0.10  # 10% height
    
    @hookimpl
    def state_wait_do(cfg, app, win, events):
        """
        In the wait state (take-a-picture), detect a tap/click in the top-right corner
        and toggle fullscreen mode.
        """
        # Only on the initial "take a picture" screen (wait state)
        # events: list of pygame events
        for ev in events:
            if ev.type in (pygame.MOUSEBUTTONUP, pygame.FINGERUP):
                # Determine click position
                if hasattr(ev, 'pos'):
                    x, y = ev.pos
                else:
                    sw, sh = win.surface.get_size()
                    x, y = ev.x * sw, ev.y * sh
                sw, sh = win.surface.get_size()
                # Define top-right region
                region_x0 = sw * (1 - REGION_WIDTH_FRAC)
                region_y1 = sh * REGION_HEIGHT_FRAC
                if x >= region_x0 and y <= region_y1:
                    # Toggle fullscreen
                    try:
                        win.toggle_fullscreen()
                        LOGGER.info("[fullscreen_toggle] Toggled fullscreen mode")
                    except Exception as e:
                        LOGGER.error(f"[fullscreen_toggle] Failed to toggle fullscreen: {e}")
                    # Consume event
                    return ev
        # Return None to fall back to default behaviorCode language: Python (python)

    Upload to WordPress

    Well, yeah. Obvi! This plugin uploads the photos to your WordPress Media library, with an optional tag and optional post object to attach them to. It will also update the URL so that it works with the “QR Code” plugin (not mine, from Pihole or something, IDK). You just have to add some of the options to your Pihole config file, like the hostname, username, application password, etc.

    import os
    import http.client
    import base64
    import json
    import pibooth
    from pibooth.utils import LOGGER
    
    __version__ = "0.0.4"
    
    SECTION = 'WORDPRESS'
    PICTURE_SECTION = 'PICTURE'
    
    @pibooth.hookimpl
    def pibooth_configure(cfg):
        """Declare the new configuration options"""
        cfg.add_option(SECTION, 'wordpress_host', '',
            "WordPress site host (e.g., 'yourwordpresssite.com')")
        cfg.add_option(SECTION, 'wordpress_username', '',
          "WordPress username")
        cfg.add_option(SECTION, 'wordpress_password', '',
            "WordPress application password")
        cfg.add_option(SECTION, 'wordpress_parent_id', '',
            "(Optional) WordPress post or page ID to attach the uploads",
            "Post or Page ID (optional)", '')
        cfg.add_option(SECTION, 'image_title', '',
            "(Optional) Title for the uploaded image",
            "Image title (optional)", '')
        cfg.add_option(SECTION, 'image_caption', '',
            "(Optional) Caption for the uploaded image",
            "Image caption (optional)", '')
        cfg.add_option(SECTION, 'wordpress_post_tag', '',
            "(Optional) Post tag taxonomy term slug to assign to the uploaded media",
            "Post tag (optional)", '')
    
    @pibooth.hookimpl
    def pibooth_startup(app, cfg):
        """Initialize the WordPress uploader plugin"""
        app.previous_picture_url = None
        wordpress_host = cfg.get(SECTION, 'wordpress_host')
        wordpress_username = cfg.get(SECTION, 'wordpress_username')
        wordpress_password = cfg.get(SECTION, 'wordpress_password')
        wordpress_parent_id = cfg.get(SECTION, 'wordpress_parent_id')
        wordpress_post_tag = cfg.get(SECTION, 'wordpress_post_tag')
        
        image_title = ''
        image_caption = ''
    
        if cfg.has_option(SECTION, 'image_title') and cfg.get(SECTION, 'image_title'):
            image_title = cfg.get(SECTION, 'image_title')
            LOGGER.info(f"Using image title from WORDPRESS config: {image_title}")
        elif cfg.has_option(PICTURE_SECTION, 'footer_text1') and cfg.get(PICTURE_SECTION, 'footer_text1'):
            image_title = cfg.get(PICTURE_SECTION, 'footer_text1')
            LOGGER.info(f"Using image title from GENERAL config: {image_title}")
        else:
            LOGGER.info("Image title missing!")
        
        if cfg.has_option(SECTION, 'image_caption') and cfg.get(SECTION, 'image_caption'):
            image_caption = cfg.get(SECTION, 'image_caption')
            LOGGER.info(f"Using image caption from WORDPRESS config: {image_caption}")
        elif cfg.has_option(PICTURE_SECTION, 'footer_text2') and cfg.get(PICTURE_SECTION, 'footer_text2'):
            image_caption = cfg.get(PICTURE_SECTION, 'footer_text2')
            LOGGER.info(f"Using image caption from GENERAL config: {image_caption}")
        else:
            LOGGER.info("Image caption missing!")
    
        if not wordpress_host:
            LOGGER.error(f"WordPress host not defined in [{SECTION}][wordpress_host], uploading deactivated")
        elif not wordpress_username:
            LOGGER.error(f"WordPress username not defined in [{SECTION}][wordpress_username], uploading deactivated")
        elif not wordpress_password:
            LOGGER.error(f"WordPress password not defined in [{SECTION}][wordpress_password], uploading deactivated")
        else:
            LOGGER.info("WordPress uploader plugin installed")
            app.wordpress_host = wordpress_host
            app.auth_token = base64.b64encode(f"{wordpress_username}:{wordpress_password}".encode()).decode()
            app.wordpress_parent_id = wordpress_parent_id if wordpress_parent_id else None
            app.wordpress_post_tag = wordpress_post_tag if wordpress_post_tag else None
            LOGGER.info(f"Post tag set to: {app.wordpress_post_tag}")
            app.image_title = image_title
            app.image_caption = image_caption
    
            LOGGER.info(f"Image title set to: {app.image_title}")
            LOGGER.info(f"Image caption set to: {app.image_caption}")
    
    @pibooth.hookimpl
    def state_processing_exit(app, cfg):
        """Upload picture to WordPress media library"""
        # Skip plugin if disabled in config
        # if not (cfg.has_section('CUSTOM_PROCESSING') and cfg.getboolean('CUSTOM_PROCESSING', 'enabled')):
        #    return
            
        if hasattr(app, 'wordpress_host') and hasattr(app, 'auth_token'):
            name = os.path.basename(app.previous_picture_file)
    
            with open(app.previous_picture_file, 'rb') as fp:
                image_data = fp.read()
    
            headers = {
                'Content-Disposition': f'attachment; filename={name}',
                'Content-Type': 'image/jpeg',
                'Authorization': f'Basic {app.auth_token}'
            }
    
            # Prepare the request path with an optional parent ID
            path = "/wp-json/wp/v2/media"
            if app.wordpress_parent_id:
                path += f"?parent={app.wordpress_parent_id}"
    
            conn = http.client.HTTPSConnection(app.wordpress_host)
            conn.request("POST", path, body=image_data, headers=headers)
            response = conn.getresponse()
            
            if response.status == 201:
                response_data = json.loads(response.read().decode())
                app.previous_picture_url = response_data['source_url']
                
                LOGGER.info(f"Uploaded image URL: {app.previous_picture_url}")
    
                # Set metadata if provided
                metadata = {}
                if app.image_title:
                    metadata['title'] = app.image_title
                if app.image_caption:
                    metadata['caption'] = app.image_caption
                if app.wordpress_post_tag:
                    term_slug = app.wordpress_post_tag
                    tag_headers = {
                        'Content-Type': 'application/json',
                        'Authorization': f'Basic {app.auth_token}'
                    }
                    # Try to fetch existing tag
                    conn.request("GET", f"/wp-json/wp/v2/tags?slug={term_slug}", headers=tag_headers)
                    tag_resp = conn.getresponse()
                    if tag_resp.status == 200:
                        tags_list = json.loads(tag_resp.read().decode())
                        if tags_list:
                            tag_id = tags_list[0]['id']
                        else:
                            # Create the tag if not found
                            payload = json.dumps({'name': term_slug, 'slug': term_slug})
                            conn.request("POST", "/wp-json/wp/v2/tags", body=payload, headers=tag_headers)
                            create_resp = conn.getresponse()
                            if create_resp.status == 201:
                                tag_id = json.loads(create_resp.read().decode())['id']
                            else:
                                LOGGER.error(f"Failed to create tag {term_slug}. Status code: {create_resp.status}")
                                tag_id = None
                    else:
                        LOGGER.error(f"Failed to fetch tag {term_slug}. Status code: {tag_resp.status}")
                        tag_id = None
                    if tag_id:
                        metadata['tags'] = [tag_id]
                        LOGGER.info(f"Assigning tag ID {tag_id} to attachment")
    
                LOGGER.info(f"Metadata to be set: {metadata}")
    
                if metadata:
                    meta_headers = {
                        'Content-Type': 'application/json',
                        'Authorization': f'Basic {app.auth_token}'
                    }
                    conn.request("POST", f"/wp-json/wp/v2/media/{response_data['id']}", body=json.dumps(metadata), headers=meta_headers)
                    meta_response = conn.getresponse()
                    if meta_response.status != 200:
                        LOGGER.error(f"Failed to set metadata. Status code: {meta_response.status}, Response: {meta_response.read().decode()}")
    
                LOGGER.info(f"Successfully uploaded {name} to WordPress. URL: {app.previous_picture_url}")
            else:
                LOGGER.error(f"Failed to upload {name}. Status code: {response.status}, Response: {response.read().decode()}")
    
            conn.close()
    
    @pibooth.hookimpl
    def pibooth_cleanup(app):
        """Cleanup actions if necessary"""
        passCode language: Python (python)

    Custom Processing (More Fun!)

    Finally, I didn’t like the generic “PROCESSING” screen that happened. I wanted it to be more personal, so I rebuilt it so that it shows the last three images taken, along with a custom waiting message taken from an array:

    #!/usr/bin/env python3
    """
    custom_processing.py
    
    A PiBooth plugin that, during the 'processing' state, grabs the last
    three saved pictures from your output directory and shows them as thumbnails.
    """
    import os
    import glob
    import pygame
    import pibooth
    from pibooth.utils import LOGGER
    from pibooth.view import background
    from pibooth import pictures
    import random
    from pibooth import fonts
    
    __version__ = "0.0.1"
    
    # A list of fun processing messages
    PROCESSING_SAYINGS = [
        "Processing…",
        "Shaking it like a Polaroid picture",
        "Now developing…",
        "Cooking your memories",
        "Almost ready!",
        "Frame by frame…",
        "Developing brilliance…",
        "Hang tight, magic in progress…",
        "Give us a sec, art is brewing…",
        "Hold on, pixels aligning…",
        "Magic happening behind the scenes…",
        "One moment please…",
        "Crafting your memories…",
        "Almost there, thanks for your patience…",
        "Don’t blink, or you might miss it…",
        "Your masterpiece is on its way…",
    ]
    
    @pibooth.hookimpl(trylast=True)
    def state_processing_enter(cfg, app, win):
        """
        When entering the 'processing' screen, list the output folder,
        pick the 3 newest image files, and render them as thumbnails.
        """
    
        # 1) Determine your output directory (default: ~/Pictures/pibooth)
        outdir = os.path.expanduser(cfg.get('PATHS', 'output_dir') if
                                    cfg.has_option('PATHS', 'output_dir') else
                                    '~/Pictures/pibooth')
        LOGGER.info(f"[custom_processing] Scanning directory: {outdir}")
    
        # 2) Glob all image files (you can tweak extensions as needed)
        exts = ('*.jpg', '*.jpeg', '*.png', '*.avif')
        files = []
        for ext in exts:
            files.extend(glob.glob(os.path.join(outdir, ext)))
        if not files:
            LOGGER.info("[custom_processing] No files found in output dir")
            return
    
        # 3) Sort by modification time and pick the last three
        files.sort(key=lambda f: os.path.getmtime(f))
        thumbs = files[-3:]
        LOGGER.info(f"[custom_processing] Last 3 files: {thumbs}")
    
        # 4) Load, scale and display as before
        surface = win.surface
        # Redraw PiBooth’s default processing background
        win._update_background(background.Background(''))
    
        sw, sh = surface.get_size()
        thumb_h = int(sh * 0.3)
        scaled = []
        for path in thumbs:
            try:
                # Load with alpha so we can mask out black
                img = pygame.image.load(path).convert_alpha()
                ow, oh = img.get_size()
                tw = int(ow * thumb_h / oh)
                img = pygame.transform.smoothscale(img, (tw, thumb_h))
    
                # Make pure black transparent (so border and photo remain)
                img.set_colorkey((0, 0, 0))
    
                scaled.append(img)
                LOGGER.info(f"[custom_processing] Loaded & scaled {os.path.basename(path)} → {(tw, thumb_h)}")
            except Exception as e:
                LOGGER.error(f"[custom_processing] Failed to load {path}: {e}")
    
        if not scaled:
            LOGGER.info("[custom_processing] No thumbnails rendered")
            return
    
        # 5) Blit and rotate thumbnails into the hanging frames with random tilt
        # Percent‐based anchor points so they adjust on resize:
        frame_positions = [
            (int(sw * 0.20), int(sh * 0.34)),  # left (moved up to 40% height)
            (int(sw * 0.50), int(sh * 0.36)),  # center (35% height)
            (int(sw * 0.80), int(sh * 0.34)),  # right (40% height)
        ]
        for img, (cx, cy) in zip(scaled, frame_positions):
            # Random rotation between -10° and +10°
            angle = random.uniform(-10, 10)
            rot_img = pygame.transform.rotozoom(img, angle, 1.0)
            rw, rh = rot_img.get_size()
            # Center the rotated thumbnail on its anchor point
            surface.blit(rot_img, (cx - rw // 2, cy - rh // 2))
    
        # Draw the transparent custom processing template over the thumbnails
        custom_bg = os.path.expanduser("~/.config/pibooth/assets/processing_custom.png")
        try:
            # Load with alpha to preserve transparency
            overlay_img = pygame.image.load(custom_bg).convert_alpha()
            # Scale to cover the full window
            overlay_img = pygame.transform.smoothscale(overlay_img, (sw, sh))
            # Blit on top of thumbnails
            surface.blit(overlay_img, (0, 0))
        except Exception as e:
            LOGGER.error(f"[custom_processing] Failed to load processing_custom overlay '{custom_bg}': {e}")
    
        # Pick and uppercase a random processing message
        say = random.choice(PROCESSING_SAYINGS).upper()
        # Define the bottom third with 5% side padding
        bottom_rect = pygame.Rect(
            int(sw * 0.05), 
            int(sh * 2/3), 
            int(sw * 0.9), 
            int(sh / 3)
        )
        # Choose a font that fits in that rectangle
        font = fonts.get_pygame_font(
            say,
            fonts.CURRENT,
            bottom_rect.width,
            bottom_rect.height
        )
        # Render the text in the window's text color
        text_color = cfg.gettuple('WINDOW', 'text_color', int)
        text_surf = font.render(say, True, text_color)
        # Center the message within the bottom third
        text_rect = text_surf.get_rect(center=bottom_rect.center)
        surface.blit(text_surf, text_rect)
    
        pygame.display.update()
        LOGGER.info("[custom_processing] Thumbnails hung with rotation")Code language: PHP (php)

    This is what it ends up looking like, anonymized:

    So yeah, use at your own risk. I don’t offer any support. Assume it will break things and burn down your house. I am not a professional. Don’t trust Python.

    Farewell!

  • Limiting Featured Image Dimensions in WordPress

    Limiting Featured Image Dimensions in WordPress

    As a follow up to my last post about limiting file sizes during uploads, I had to come back to the problem with limiting image sizes for featured images. Not bytes this time, but pixel dimensions.

    Still being a bit of a block editor newb, this was an interesting challenge for me, and I was really surprised at how easy it was to implement. I basically googled around and found a few different things to put together that worked for me. The primary source was a post by Igor Benic on how to disable the publish button:

    After that, I discovered how to add notifications to the block editor, to tell when the image was too large, thanks to a post by David F. Carr:

    So what’s it look like? Well, it’s pretty simple. If you choose a featured image that’s too large for the settings, it will add a non-dismissible error notification to the editor:

    Then it will block you from hitting the publish button:

    So, finally the code. This is just enqueued in as a simple add_action() during the enqueue_block_editor_assets hook. I’m absolutely not a good JS developer, so please don’t judge me too harshly. Also, that means this could be riddled with bugs. Use at your own risk 😀

    var postLocked = false;
    wp.domReady( () => {
    	wp.data.subscribe( function() {
    		var imageId = wp.data.select( 'core/editor' ).getEditedPostAttribute( 'featured_media' ); // Featured Image ID.
    
    		// If we have no image ID, and we already locked the post, we won't do anything.
    		if ( imageId ) {
    			blockEditorSettings = wp.data.select('core/block-editor').getSettings();
    
    			// Default to 1200px wide.
    			maxImageSize = 1200;
    			imageAttrs = wp.data.select('core').getMedia( imageId );
    
    			// Get the size for the "large image" and if it's available, use that instead.
    			if ( typeof blockEditorSettings !== 'undefined' && blockEditorSettings.hasOwnProperty( 'imageDimensions' ) ) {
    				maxImageSize = blockEditorSettings.imageDimensions.large.width;
    			}
    
    			if ( typeof imageAttrs !== 'undefined' && imageAttrs.hasOwnProperty( 'media_details') ) {
    				// Publish is not locked and width is too large.
    				if ( ! postLocked && imageAttrs.media_details.width > maxImageSize ) {
    					postLocked = true;
    					wp.data.dispatch( 'core/editor' ).lockPostSaving( 'featuredImageTooLarge' );
    
    					wp.data.dispatch('core/notices').createNotice(
    						'error', // Can be one of: success, info, warning, error.
    						wp.i18n.__( wp.i18n.sprintf( 'Featured image width must be less than %spx, currently %spx. Publishing is disabled.', maxImageSize, imageAttrs.media_details.width ) ),
    						{
    							id: 'featuredImageTooLarge', // Assigning an ID prevents the notice from being added repeatedly.
    							isDismissible: false, // Whether the user can dismiss the notice.
    						}
    					);
    				}
    
    			}
    		} else if ( postLocked ) {
    			postLocked = false;
    			wp.data.dispatch( 'core/editor' ).unlockPostSaving( 'featuredImageTooLarge' );
    			wp.data.dispatch('core/notices').removeNotice( 'featuredImageTooLarge' );
    		}
    	} );
    } );Code language: JavaScript (javascript)
  • Half Baked Idea: Limiting WordPress Image Upload Sizes

    Half Baked Idea: Limiting WordPress Image Upload Sizes

    If you want to be able to limit images (or any attachments) from being uploaded into the media library if they are too large, you can use the wp_handle_upload_prefilter filter to do this.

    Below is a really basic example I whipped up in a few minutes that I shared with a customer not too long ago:

    /**
     * Limits the maximum size of images that can be uploaded.
     *
     * @param array $file {
     *     Reference to a single element from `$_FILES`.
     *
     *     @type string $name     The original name of the file on the client machine.
     *     @type string $type     The mime type of the file, if the browser provided this information.
     *     @type string $tmp_name The temporary filename of the file in which the uploaded file was stored on the server.
     *     @type int    $size     The size, in bytes, of the uploaded file.
     *     @type int    $error    The error code associated with this file upload.
     * }
     *
     * @return array       Filtered file array, with a possible error if the file is too large.
     */
    function blarg_limit_max_image_upload_size( $file ) {
        if ( str_starts_with( $file['type'], 'image/' ) ) {
            $max_size = 1 * 1024; // 1kb.
    
            if ( $file['size'] > $max_size ) {
                // translators: %s: Human readable maximum image file size.
                $file['error'] .= sprintf( __( 'Uploaded images must be smaller than %s.' ), size_format( $max_size ) );
            }
        }
    
        return $file;
    
    }
    add_filter( 'wp_handle_upload_prefilter', 'blarg_limit_max_image_upload_size', 10, 1 );Code language: PHP (php)

    Good luck if you use any of my hot garbage in production 😀

  • Dall-E 2 Outpainting Experiment

    Dall-E 2 Outpainting Experiment

    OpenAI recently introduced a new feature called Outpainting to Dall-E 2

    https://x.com/OpenAI/status/1565009319447314432?ref_src=twsrc%5Etfw%7Ctwcamp%5Etweetembed%7Ctwterm%5E1565009319447314432%7Ctwgr%5Efed2d0e7c9337da1fb5e0ab9ec7bba462e57afe1%7Ctwcon%5Es1_c10&ref_url=https%3A%2F%2Fdataconomy.com%2F2022%2F09%2Fdall-e-2-launched-outpainting%2F

    This lets you easily expand your image without manual labor intensive hacks.

    With this, I decided to do a quick run through to see what’s on the other side of Bliss

    https://en.wikipedia.org/wiki/Bliss_(image)

    Now, I probably could have gotten something a lot better if I’d taken my time and adjusted my prompt, but this is good for a fun experiment:

    Feel free to download the raw file straight from DALL-E 2 if you’d like to do anything with it:

  • Dynamic Tea House

    Dynamic Tea House

    Not too long ago, a coworker shared that they had created a WordPress Dynamic Wallpaper for macOS. I thought this was just cool, so I eventually made one of my own with my favorite dynamic set of images: the Tea House theme from Gmail.

    Now, these classic Gmail themes are all but lost to history, and that’s a huge shame.

    With the help of some code I found on GitHub, I was able to download and reconstruct the theme as a desktop background. Unfortunately, the image sizes are rather small for modern desktops, so I also used bigjpg.com and the waifu2x image scaling algorithm to upcale these to over 5K in size:

    You can download these at Dynamic Wallpaper Club, or from the links below:

    tea-house.heic.zip

    tea-house-9×16.heic.zip

    I’ve got to thank the amazing folks at Meomi for their artwork I’ve stolen without asking. I hope they can forgive me!

  • Sun Dog

    Sun Dog

    While driving today I noticed some really strong sun dogs and I just had to take a picture 🙂

    It’s edited a little bit to help make it clearer through my car window, but it was still awesome!

  • Fowler Park Pioneer Days

    Fowler Park Pioneer Days

    Still trying to figure out this iPhone camera.

    Bonus: Covered bridge and a trash can:

  • Validating WordPress attachments with WP-CLI

    Validating WordPress attachments with WP-CLI

    I recently worked on migrating a site to a different server and for one reason or another, some of the images did not come over properly. While I could have just re-downloaded and re-imported all of the media, it would have taken quite a while since the media library was well over 100Gb. Instead, I opted to use WP-CLI to help find what images were missing:

    (more…)
  • Photography Woes

    Photography Woes

    My morning routine usually involves me checking my RSS reader NewsBlur.  One of the many, many sites I follow is the Greene County Daily World, my “local” newspaper.

    I was surprised when I saw an article titled “Old Clifty Church in need of repair” that had an eerily familiar image.

    Because it was mine.

    (more…)