6

I'm working on a script based on this question: What steps are needed to create a new user from the command line on Mountain Lion?

The script is packaged up in an empty installer as a postflight script.

It's all working well enough - however, I cannot get the Account picture set. I can use dscl to set the Picture property to an image file, for example the default Apple Account photos.

I can even copy a photo I include in the package over to a number of directories, including /Users/newAccount/Public. But the permissions are messed up and no one but the newAccount can view the profile image. I am unable to chmod the file. Instead, I have to use the GUI and "get info -> Set Permissions"

The other option that I can see is if I set the JPEGPhoto value. This is what happens when you use a custom photo or take one in photobooth. However, I'm not sure how to convert the photo I include in the installer into Hex or Whatever is being stored in the JPEGPhoto value.

Thanks! - Chris

Edit: Added script. Everything works in this script - the picture just won't display, due to permissions. The path is set properly. I've tried to place it several different locations that have shared access, but no matter what the permissions are wrong and I cannot chmod them.

###CREATE TP ADMINISTRATOR ACCOUNT###

LastID=`dscl . -list /Users UniqueID | awk '{print $2}' | sort -n | tail -1`

NextID=$((LastID + 1))
. /etc/rc.common
dscl . create /Users/administrator

dscl . create /Users/administrator RealName "Administrator Account"
dscl . create /Users/administrator hint "the password Duh"

dscl . passwd /Users/administrator password
dscl . create /Users/administrator UniqueID $NextID
dscl . create /Users/administrator PrimaryGroupID 80
dscl . create /Users/administrator UserShell /bin/bash
dscl . create /Users/administrator NFSHomeDirectory /Users/administrator
cp -R /System/Library/User\ Template/English.lproj /Users/administrator
chown -R administrator:staff /Users/administrator

cp $1"/Contents/Resources/admin.jpg" /Users/administrator/Public
dscl . create /Users/administrator Picture "/Users/administrator/Public/admin.jpg"

###CREATE TP ADMINISTRATOR ACCOUNT###
4
  • Chris, could you show us the script you currently have? It should work fine to set the 'picture' value to one of the Apple default ones. Commented Jan 17, 2014 at 6:03
  • Also, you realise you are only setting the login icon here? Oh, and are you being careful of spaces in the path? Commented Jan 17, 2014 at 6:16
  • Hi Tony, as I said I am able to set it to the apple default pictures. And the path is correct, but when I move the file I end up with permissions that only allow root to view the image. I am unable to chmod the file.
    – Dakine83
    Commented Jan 20, 2014 at 18:15
  • Yep, I know it's just the picture, and not that important. It was more of a frustration than a requirement. I will post the script shortly, though I mangled it in an attempt to get the permissions working properly.
    – Dakine83
    Commented Jan 20, 2014 at 18:16

2 Answers 2

6

dscl

It seems that the most promising method is to modify the JPEGPhoto attribute for the user. The problem is, while the JPEG image can be converted very simply to a hex string, the value is too long to just pass at the command-line. This attribute seems to be what the GUI writes to when you drag in an image. Recovering the image from this variable when set is as simple as:

dscl . read /Users/username JPEGPhoto | xxd -r -p > ./username.jpg

The second common way mentioned on the forums is to:

dscl . delete /Users/username JPEGPhoto
dscl . delete /Users/username Picture
dscl . delete /Users/username Picture "/Library/User Pictures/username.jpg"

This only changes the 'login' icon and not the icon seen in the User detail page in System Preferences.

Using dsimport

What does work for changing the user image is dsimport.

The script usage is:

./change_userpic.sh USERNAME /path/to/image/jpg

GIST

#!/bin/bash
set -e

declare -x USERNAME="$1"
declare -x USERPIC="$2"

declare -r DSIMPORT_CMD="/usr/bin/dsimport"
declare -r ID_CMD="/usr/bin/id"

declare -r MAPPINGS='0x0A 0x5C 0x3A 0x2C'
declare -r ATTRS='dsRecTypeStandard:Users 2 dsAttrTypeStandard:RecordName externalbinary:dsAttrTypeStandard:JPEGPhoto'

if [ ! -f "${USERPIC}" ]; then
  echo "User image required"
fi

# Check that the username exists - exit on error
${ID_CMD} "${USERNAME}" &>/dev/null || ( echo "User does not exist" && exit 1 )

declare -r PICIMPORT="$(mktemp -t ${USERNAME}_dsimport)" || exit 1
trap 'rm -f ${PICIMPORT}' EXIT

printf "%s %s \n%s:%s" "${MAPPINGS}" "${ATTRS}" "${USERNAME}" "${USERPIC}" >"${PICIMPORT}"
${DSIMPORT_CMD} "${PICIMPORT}" /Local/Default M &&
    echo "Successfully imported ${USERPIC} for ${USERNAME}."
4
  • How about if the current user 'photo' is one of the default icons in Tif format?
    – codecowboy
    Commented Jan 2, 2020 at 8:46
  • dscl . delete /Users/username Picture Commented Jun 1, 2021 at 14:40
  • 1
    pro tip: instead of using rm $TMPFILE which might not execute due to circumstances outside of your control, do trap 'rm $TMPFILE' 0 right after the mktemp command.
    – Otheus
    Commented May 21 at 10:49
  • @Otheus that is a reasonable recommendation. Certainly as an example for others to consider writing better BASH scripts. Commented May 26 at 10:43
4

Having pained over this for a couple of days, I thought I'd share what have found out about this...

User data is stored in the "Open Directory", like Apple's version of OpenLDAP or ActiveDirectory. Old documentation here. There are two user data items containing images:

  • The original picture data (goes back to early OSX versions) is called Picture and contains a path to an image on the file system. It does not have to be JPEG but the path is limited to 255 UTF-8 characters.
  • The JPEGPhoto, is a JPEG-formatted image that is embedded into the user record (in contrast to Picture which points to a file on the file system). It seems to have been introduced around OSX 10.5/6.
  • If a user has both JPEGPhoto and Picture then the Picture is ignored (at least by the login and user preferences screens).
  • The JPEGPhoto is stored as base64-encoded JPEG image data.

You can use dscl (directory service command line) to query the directory and extract image data. If you do this then the image data is presented as hexadecimal data (i.e. the base64 is decoded into hex):

dscl . read /Users/alice JPEGPhoto

You can decode the hex data into an image file:

dscl . read /Users/alice JPEGPhoto | tail -1 | xxd -r -p > photo.jpg

(a quick way to view an image from the macos command line is qlmanage -p photo.jpg)

Deleting images is also done using dscl:

dscl . delete /Users/alice JPEGPhoto
dscl . delete /Users/alice Picture

You can also use dscl to load data:

dscl . read /Users/alice Picture /path/to/alice.jpg

That's handy for the Picture but JPEGPhoto data is unlikely to fit on the command line as an argument. Instead, you can use dsimport to import JPEGPhoto. This requires that you create an import file as described here which distils to the following pattern seen in other answers, articles and blog posts elsewhere:

0x0A 0x5C 0x3A 0x2C dsRecTypeStandard:Users 2 dsAttrTypeStandard:RecordName externalbinary:dsAttrTypeStandard:JPEGPhoto
alice:/path/to/alice.jpg

This imports the image at the given path into the JPEGPhoto of the user's record, storing it internally as base64-encoded binary data. An alternative way to do this is to pass the encoded image data directly:

0x0A 0x5C 0x3A 0x2C dsRecTypeStandard:Users 2 dsAttrTypeStandard:RecordName base64:dsAttrTypeStandard:JPEGPhoto
alice:<base64 data>

To summarise the import file format, the four hex characters at the beginning specify the data format:

  • 0x0A End of record is indicated by a new line
  • 0x5C The escape character is defined as \
  • 0x3A The field separator is a :
  • 0x2C The value separator is a , (comma)

These values, which are the only ones ever seen in examples, define the data format such that each record is a line on its own, terminated by a newline. The record is composed of fields separated by colons and multi-value fields separate values using commas. Should there be a need to escape either of those characters then the escape character is \.

Following those four hex values is the record type (here we have dsRecTypeStandard:Users for user records), the number of fields (2) and what they are (dsAttrTypeStandard:RecordName - which is the username - and dsAttrTypeStandard:JPEGPhoto). The externalbinary prefix tells dsimport how to interpret the field, in this case as the path to an image file (there are a number of these including base64, utf8 and externalutf8 but documentation about this appears lacking).

Recall that the JPEGPhoto is stored as base64-encoded data - dsimport performs any required conversion (in the case of base64, as used above, there is no conversion).

You import the prepared import file like this:

dsimport dsimport_file /Local/Default M

Again, a little explanation: /Local/Default is the path of the Open Directory and M requests that import data is merged where data exists. See the man page for dsimport. You can add --loglevel n where n is between 0 and 3 (Command Line documentation, page 118) and view log files written to ~/Library/Logs/ImportExport.

If replacing an image, it's necessary to delete any existing image first (use dscl, described above) otherwise the image doesn't update.

So, to summarise, two examples. First passing the image file's path in the import file:

#!/bin/sh
user="$1"
image="$2"
dscl . delete /Users/$user JPEGPhoto
dscl . delete /Users/$user Picture
tmp="$(mktemp)"
printf "0x0A 0x5C 0x3A 0x2C dsRecTypeStandard:Users 2 dsAttrTypeStandard:RecordName externalbinary:dsAttrTypeStandard:JPEGPhoto\n%s:%s" "$user" "$image" > "$tmp"
dsimport "$tmp" /Local/Default M
rm "$tmp"

And, secondly, encoding the image in the import file:

#!/bin/sh
user="$1"
image="$2"
dscl . delete /Users/$user JPEGPhoto
dscl . delete /Users/$user Picture
tmp="$(mktemp)"
printf "0x0A 0x5C 0x3A 0x2C dsRecTypeStandard:Users 2 dsAttrTypeStandard:RecordName base64:dsAttrTypeStandard:JPEGPhoto\n%s:%s" "$user" "$(base64 "$image")" > "$tmp"
dsimport "$tmp" /Local/Default M
rm "$tmp"

Use like this:

$ sudo add-photo username /path/to/photo.jpg

Footnote

I am using this in an Ansible role to update users' avatars on multiple Apple Macs. Ansible is run on a Linux box which is why the embedded image method was interesting to me because I didn't need to write the image to each Mac's disk. In case anyone finds it useful:

- name: User avatars
  become: true
  ansible.builtin.shell:
    cmd: |
      tmp="$(mktemp)"
      printf "0x0A 0x5C 0x3A 0x2C dsRecTypeStandard:Users 2 dsAttrTypeStandard:RecordName base64:dsAttrTypeStandard:JPEGPhoto\n%s:" "{{item.name}}" > "$tmp"
      cat >> "$tmp"
      dscl . delete /Users/{{ item.name }} JPEGPhoto
      dsimport "$tmp" /Local/Default M
      rc=$?
      rm "$tmp"
      exit $rc
    stdin: "{{lookup('file', item.avatar) | b64encode }}"
  loop:
    "{{ users | selectattr('avatar', 'defined') }}"

The users passed in is a list having a name and avatar value for each.

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .