How to export Preview jpeg at a specific kb size?

Hi KM forum,

I have a simple task that seems challenging with KM. When trying to resize an image in mac's Preview app to make it smaller, we do File > Export...

Trying to choose a fixed file size to export, like 100 KB for example via the slider, but can't figure out how to automate it in KM.

Here's a demo of what we're trying to automate:

Any idea how to accomplish this?

Note: This is the final step in a preview automation macro

I think that would be really tricky to automate in Preview. The only way I can think of to do it would be by watching for an image that contains "100" in the location where the size is. You'd have to move the slider a bit, check the image, repeat. And that seems like it'd be barely workable.

If you're open to adding another tool, what if the last step in Preview just saves the JPEG, and then you use another tool to set its size? There are any number of command line tools that can do this. Them monster of them all is ImageMagick, but that's overpowered for a task like this.

In the paste, I've used jpegoptim for similar tasks, and it works well. I never used it to set a size limit, but it looks like the syntax for that is jpegoptim -S100 YourFileHere.jpg. You could use an Execute Shell Script to run the command on your saved image.

Both of the above can be installed using Homebrew or (I assume) MacPorts, tools that make it easy to install various Unix apps on macOS.

If this isn't a viable solution (due to needing to share with others, restrictions on a work Mac, etc.), I understand ... but I don't see a way to cleanly do what you want using just Preview and Keyboard Maestro.

-rob.

I politely disagree. This is a perfect case for the use of the new Apple OCR in KM.

EDIT...

I just wrote a solution using OCR. Here it is. It will probably require several changes to make it work for other people (due to screen resolution, and other things) so in this case I won't upload the macro itself, just a screenshot, because anyone who wants to use this will have to think about it and change most of the actions anyway. Without making those changes, the macro would fail.

image

The first MOVE action drags the bar to the left. The second MOVE action moves the bar 5 pixels to the right (you can change 5 to any value you want. I found that 1 was much too slow.) The first UNTIL condition pauses if the text contains the word "Calculating" which happens a lot. The second UNTIL condition waits until it finds a three digit number starting with 1. The magenta actions display what the macro is seeing, which is interesting and harmless, but you may want to delete it.

There are ways to make my code more bulletproof and flexible, but I didn't want to complicate matters. It could also be made a little simpler, but then it might miss 100 and skip ahead to 105. If the speed is a problem, it's entirely possible to modify this to a binary search and then it would be much faster.

I knew OCR could do it, but I (personally) would still use a tool that doesn't rely on manipulating onscreen controls to achieve the results I want—I'm a big fan of staying away from screen manipulation unless absolutely required.

With that said, your solution is 100% better than mine, given the objective to work solely in Preview. It's just not one that I would personally use, so I tend not to think about screen manipulation solutions :).

-rob.

How about we split the difference and use UI scripting in Preview :wink:

Pop this AppleScript in as the action after you've opened the Export pane (you may need a slight delay to let the sheet appear) and you should be good -- it works well in testing:

tell application "System Events"
	tell application process "Preview"
		tell window 1
			tell sheet 1
				set currSize to value of last item of (every UI element whose role is "AXStaticText")
				set {sizeNumber, sizeUnits} to splitText(currSize) of me
				repeat while sizeUnits is not "KB"
					set value of slider 1 to (value of slider 1) - 0.1
					set currSize to value of last item of (every UI element whose role is "AXStaticText")
					set {sizeNumber, sizeUnits} to splitText(currSize) of me
				end repeat
				repeat while sizeNumber > 100
					set value of slider 1 to (value of slider 1) - 0.1
					set currSize to value of last item of (every UI element whose role is "AXStaticText")
					set {sizeNumber, sizeUnits} to splitText(currSize) of me
				end repeat
				repeat while sizeNumber < 100
					set value of slider 1 to (value of slider 1) + 0.01
					set currSize to value of last item of (every UI element whose role is "AXStaticText")
					set {sizeNumber, sizeUnits} to splitText(currSize) of me
				end repeat
			end tell
		end tell
	end tell
end tell

on splitText(fileSizeText)
	set theNum to first word of fileSizeText as number
	set theUnits to last word of fileSizeText
	return {theNum, theUnits}
end splitText

Execute an AppleScript.kmactions (2.0 KB)

It works by stepping the slider down, 0.1 units (a 10th of the slider) at a time, until we start measuring in "KB". Then it carries on, still 0.1 units at a time, until the number reported is less than 100. It then steps up 0.01 units at a time while the reported number is less than 100.

End result will be 100KB or just over (eg if you reduce to 99KB then step up it'll be 99KB + 1% of "maximum" file size). If you really have an hard limit of 100KB (you didn't in the video) you could set the final repeat to < 99 and/or only increment by + 0.001 (smaller increments make it all take longer, of course).

2 Likes

Thank you all so much for your solutions, they're all great answers. I went with @Nige_S solution as it utilized both scripting and Preview, was fairly quick, and learned a lot from the approach.

Try this AppleScript instead -- it uses a "binary search" method to zero in on "100 KB" and will be both faster and more precise on the final size. (There's also a loop to cope with large images, for which a compression value change will return "Calculating..." before the file size updates.)

Edit: This script is broken -- commented to show where. Fixed version in post below.

tell application "System Events"
	tell application process "Preview"
		tell window 1
			tell sheet 1
				set highValue to 1
				set lowValue to 0
				set currSize to value of last item of (every UI element whose role is "AXStaticText")
				repeat while currSize is not "100 KB"
					set {sizeNumber, sizeUnits} to splitText(currSize) of me
					if sizeNumber > 100 or sizeUnits is not "KB" then
						set highValue to value of slider 1
					else
						if sizeNumber < 100 then set lowValue to value of slider 1
					end if
					set newValue to ((lowValue + highValue) / 2)
					set value of slider 1 to newValue
					if newValue < 1.0E-3 or newValue > 0.999 then exit repeat
					--this next bit is broken - AS "repeat until" isn't guaranteed to run at least one, unlike other languages
					repeat until currSize does not contain "Calculating"
						set currSize to value of last item of (every UI element whose role is "AXStaticText")
					end repeat
				end repeat
			end tell
		end tell
	end tell
end tell

on splitText(fileSizeText)
	set theNum to first word of fileSizeText as number
	set theUnits to last word of fileSizeText
	return {theNum, theUnits}
end splitText
1 Like

Hmm, the last Binary look up script seems to not work, it drags the slider all the way to the min size ~ 56 KB

That's my bad -- I messed up the repeat until, assuming it would run at least once (like in other languages). I'm a bit rusty, I'm afraid.

Try this:

tell application "System Events"
	tell application process "Preview"
		tell window 1
			tell sheet 1
				set highValue to 1
				set lowValue to 0
				set currSize to value of last item of (every UI element whose role is "AXStaticText")
				repeat while currSize is not "100 KB"
					set {sizeNumber, sizeUnits} to splitText(currSize) of me
					if sizeNumber > 100 or sizeUnits is not "KB" then
						set highValue to value of slider 1
					else
						if sizeNumber < 100 then set lowValue to value of slider 1
					end if
					set newValue to ((lowValue + highValue) / 2)
					set value of slider 1 to newValue
					if newValue < 1.0E-3 or newValue > 0.999 then exit repeat
					set currSize to value of last item of (every UI element whose role is "AXStaticText")
					repeat until currSize does not contain "Calculating"
						set currSize to value of last item of (every UI element whose role is "AXStaticText")
					end repeat
				end repeat
			end tell
		end tell
	end tell
end tell

on splitText(fileSizeText)
	set theNum to first word of fileSizeText as number
	set theUnits to last word of fileSizeText
	return {theNum, theUnits}
end splitText
1 Like

The latest binary script seems to work for 100 KB, when playing around with the numbers setting it to 115 KB, it's stuck in a loop:
CleanShot 2024-03-29 at 18.51.52

I saw the same thing here in my testing, as I was trying to compare my method to the Preview method. I reverted to the previous script, which worked fine.

And this is a perfect example of perhaps one of us should have asked what the actual task at hand was, and did it require Preview? And do the images have to be exactly 100KB?

If Preview isn't required, and something really close to 100KB is acceptable, then I'd argue that jpegoptim is the way to go. Instead of all the above code, this two-line shell script does all the same work:

/opt/homebrew/bin/jpegoptim -S100 /path/to/testimage.jpeg --dest=/path/to/new_image_folder -w16
mv /path/to/new_image_folder/testimage.jpeg /path/to/testimage-shell.jpeg

.

The second command isn't strictly required—I did it so that both macros would save the modified image to the same folder as the original. And because jpegoptim defaults to overwriting the original file, and only lets you specify a path to a folder for modified images, I have to move and rename the final result.

Now why would you maybe prefer the above unreadable mess to the Preview/AppleScript solution? Speed and multi-file processing.

I tested with a 198KB 2048x1152 source image, and two different macros: Preview/AppleScript and jpegoptim. In my testing, Keyboard Maestro required about 2.5 seconds to save the image using the Preview/AppleScript solution. Here's that macro:

It selects the Export menu, waits until the Save button is enabled (which means the dialog has loaded), then runs the AppleScript. It then renames the file and clicks the Save button. Over a few runs, I saw times between 2.2 and 2.6 seconds.

The shell script version is much simpler:

That's just the shell commands from above, without my drive info visible :). So how long did that take? 0.15 seconds, or about 16x faster than Preview/AppleScript. The final file size was 103KB, so if exactly 100KB is required, then my method is out (though you could specify 95 instead of 100 in the Terminal command, and that would probably insure you were always at or under 100KB.)

But the big advantage of this method, beyond the speed, is that there's absolutely nothing onscreen. So if you have to process a folder of images, just put the shell script in a "For each path in Finder selection" loop, and it'll do the whole batch without making your screen load each image.

I did just that with a folder of 63 images whose original sizes were between 4MB and 8MB—they're 5K desktop images. Pretty tough task, asking those to be reduced to 100KB without changing their size!

It took the macro 33 seconds to process the entire folder—this would've been about 160 seconds using Preview. But most importantly, my Mac was fully usable during that time, because the processing was happening in the shell, without any windows opening and closing onscreen.

Please note I am not arguing the shell is always a superior method of getting things done. Often, it's not. And it's got some downsides with readability, portability, etc. But for some tasks, it can be dramatically faster, and once you know the syntax, simpler—there's absolutely no way I could write the masterful AppleScript that @Nige_S came up with. But figuring out the parameters for one shell command? That I can usually do :).

Just a different perspective—and just so everyone's clear, I think @Nige_S' solution is amazingly slick. If Preview and exactly 100KB are requirements, that script is a thing of beauty! :slight_smile:

-rob.

2 Likes

Thank you @griffman for the detailed post and I totally agree; @Nige_S's script is a thing of beauty!

I was really curious how to get the slider element to set a fixed size, since I didn't see a programmatic way to do it, like an input field in Preview for example. I use Preview to make a few other image manipulations first, like resizing, modifying the resolution, and the export part was the last piece that had to be done manually. I figured someone here would probably know of a solution and you guys provided some really interesting solutions. Thank you guys so much for your input; this is a truly amazing KMunity :slight_smile:

1 Like

LOL!!

Gah! So out of practice... The problems started when I tried to cover images that were too big or too small to ever reach 100KB -- I'll try again later.

But...

UI scripting is inherently fragile. For example, there's no "fixed" identifier for the "Size" text field we're testing, it's whatever the text is set to at the time, which is why we're having to grab the last text field on the sheet. But that'll break if Apple ever add another field after it.

I'm with @griffman, with a preference for using command line utilities for this sort of thing -- especially if no human intervention is required. If all you're doing is some resizing, setting the DPI, etc -- stuff that doesn't require someone to use Preview for decision-making, like where to make a crop -- then I'd use the sips utility that comes with the OS to resize then jpegoptim to do the compression. And you could probably loop the jpegoptim part with increasing/decreasing values for -S, like we did with the AS, if you wanted to hit 100KB and not just approximate it.

It'd be a fun project (and I, for one, obviously need the practice!) -- so how about you list the steps in your workflow and we'll see what we can do?

1 Like

Probably one of the the least-appreciated least-known Unix apps that comes on every Mac; it can do some very useful image manipulation. I wrote a brief thing about it some years back, designed for those new to Terminal.

-rob.

2 Likes

Wow, I didn't know about this. It looks like I could use this to change the DPI of a file, which is one of the things I've always needed to do in KM.

Maybe take a look at this KM action; action:Resize Image [Keyboard Maestro Wiki]

2 Likes

I think this fixes things -- works on all the files I've tried it on, anyway:

tell application "System Events"
	tell application process "Preview"
		tell window 1
			tell sheet 1
				set highValue to 1
				set lowValue to 0
				set currSize to value of last item of (every UI element whose role is "AXStaticText")
				repeat while currSize is not "100 KB"
					set {sizeNumber, sizeUnits} to splitText(currSize) of me
					if sizeNumber > 100 or sizeUnits is not "KB" then
						set highValue to value of slider 1
					else
						if sizeNumber < 100 then set lowValue to value of slider 1
					end if
					set newValue to ((lowValue + highValue) / 2)
					set value of slider 1 to newValue
					set testVal to (round (newValue * 100)) / 100
					if testVal = 0 or testVal = 1 then
						set value of slider 1 to testVal
						exit repeat
					end if
					set currSize to value of last item of (every UI element whose role is "AXStaticText")
					repeat until currSize does not contain "Calculating"
						set currSize to value of last item of (every UI element whose role is "AXStaticText")
					end repeat
				end repeat
			end tell
		end tell
	end tell
end tell

on splitText(fileSizeText)
	set theNum to first word of fileSizeText as number
	set theUnits to last word of fileSizeText
	return {theNum, theUnits}
end splitText

Thanks again @Nige_S, I tried it but when I change the size to say 115 KB it's still stuck in a loop, you guys have provided some already working, great solutions.

Your first script is still super fast for my use case, as I am just doing a single image at a time. Occasionally, some visual cropping is involved, which is why I prefer the Preview approach for now. However, you and @griffman's blog post have opened my eyes to yet another possibility of sips a native, built in utility which I had no idea about, and appears to be super useful. Thank you guys for sharing that; I'll definitely play around with it over time to explore it further. It would've been great to have sips output at an exact KB size IMO, instead of a % or normal/best/etc param, then having to use a 3rd party tool just for the file size.

Hmm... Works fine here. Did you change the value in all three places (lines 8, 10, and 13)?

It would better anyway to use variables for the required size, so it only has to be set once in the script -- especially if you are going to be changing it often.

It really depends on your workflow -- I thought this was an "always size to 100KB, without user intervention", but if you want "size to user-chosen value, via dialog", "size to destination-specific value, by web service", or something else, then you need to state your requirements.