How to read/write the content of MS Office XML file?

I would like to get or set the the value of all keys (name, dk1, accent1, etc.) in this kind of XML file content:

 <?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<a:clrScheme xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main"
    name="BaseTheme">
    <a:dk1><a:srgbClr val="000000"/></a:dk1>
    <a:lt1><a:srgbClr val="FFFFFF"/></a:lt1>
    <a:dk2><a:srgbClr val="000000"/></a:dk2>
    <a:lt2><a:srgbClr val="FFFFFF"/></a:lt2>
    <a:accent1><a:srgbClr val="808080"/></a:accent1>
    <a:accent2><a:srgbClr val="808080"/></a:accent2>
    <a:accent3><a:srgbClr val="808080"/></a:accent3>
    <a:accent4><a:srgbClr val="808080"/></a:accent4>
    <a:accent5><a:srgbClr val="808080"/></a:accent5>
    <a:accent6><a:srgbClr val="808080"/></a:accent6>
    <a:hlink><a:srgbClr val="505050"/></a:hlink>
    <a:folHlink><a:srgbClr val="A0A0A0"/></a:folHlink>
</a:clrScheme>

And how to convert a RGB color to Hexadecimal (e.g. {255,255,225} to “FFFFFF”?

Thaaaaaaaanx!
:wink:

This should give you enough to get started:

use AppleScript version "2.4" -- Yosemite (10.10) or later
use framework "Foundation"
use scripting additions

set theText to " <?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"yes\"?>
<a:clrScheme xmlns:a=\"http://schemas.openxmlformats.org/drawingml/2006/main\"
    name=\"BaseTheme\">
    <a:dk1><a:srgbClr val=\"000000\"/></a:dk1>
    <a:lt1><a:srgbClr val=\"FFFFFF\"/></a:lt1>
    <a:dk2><a:srgbClr val=\"000000\"/></a:dk2>
    <a:lt2><a:srgbClr val=\"FFFFFF\"/></a:lt2>
    <a:accent1><a:srgbClr val=\"808080\"/></a:accent1>
    <a:accent2><a:srgbClr val=\"808080\"/></a:accent2>
    <a:accent3><a:srgbClr val=\"808080\"/></a:accent3>
    <a:accent4><a:srgbClr val=\"808080\"/></a:accent4>
    <a:accent5><a:srgbClr val=\"808080\"/></a:accent5>
    <a:accent6><a:srgbClr val=\"808080\"/></a:accent6>
    <a:hlink><a:srgbClr val=\"505050\"/></a:hlink>
    <a:folHlink><a:srgbClr val=\"A0A0A0\"/></a:folHlink>
</a:clrScheme>"

set theDoc to current application's NSXMLDocument's alloc()'s initWithXMLString:theText options:(current application's NSXMLDocumentTidyXML) |error|:(missing value)
set theRoot to theDoc's rootElement()
theRoot's attributeForName:"name" -- return stringValue() or use setStringValue:
set rgbElement to ((theRoot's elementsForName:"a:dk1")'s firstObject()'s elementsForName:"a:srgbClr")'s firstObject()
set anAttribute to rgbElement's attributeForName:"val"
anAttribute's setStringValue:"FFFFFF"

Google will point you to several AppleScript handlers.

The option NSXMLDocumentTidyXML modifies the content so I chose NSXMLDocumentValidate.
I’m I right?

use AppleScript version "2.4" -- Yosemite (10.10) or later
use framework "Foundation"
use scripting additions
use framework "AppKit"

set thePath to "~/Library/Application Support/Microsoft/Office/Modèles utilisateur/Mes thèmes/Theme Colors/aaaaaa.xml"
set thePath to current application's NSString's stringWithString:thePath
set thePath to thePath's stringByExpandingTildeInPath()
set theURL to current application's NSURL's fileURLWithPath:thePath
set theDoc to current application's NSXMLDocument's alloc()'s initWithContentsOfURL:theURL options:(current application's NSXMLDocumentValidate) |error|:(missing value)
set theRoot to theDoc's rootElement()
(theRoot's attributeForName:"name")'s setStringValue:"aaaaaa"
set anAttribute to ((theRoot's elementsForName:"a:accent4")'s firstObject()'s elementsForName:"a:srgbClr")'s firstObject()'s attributeForName:"val"
anAttribute's setStringValue:"FF0000"
set theData to theDoc's XMLData()
theData's writeToURL:theURL atomically:true

tell application id "com.microsoft.Powerpoint" -- Microsoft PowerPoint
	activate
	load theme color scheme (theme color scheme of theme of slide master of active presentation) file name (theURL as text)
end tell

About the RGB-HEX conversion, I thought there was an AppleScriptObjC method, but the only thing I found is not retuning the expected result:

 current application's NSString's stringWithFormat_("#%02X%02X%02X", {theRed, theGreen, theBlue})

If I understand you well, there’s only the Vanilla AS syntax that’s possible?

convertRGBColorToHexValue({238, 115, 117})
on convertRGBColorToHexValue(theRGBValues)
	set theHexList to {"0", "1", "2", "3", "4", "5", "6", "7", "8", "9", "A", "B", "C", "D", "E", "F"}
	set theHexValue to ""
	repeat with a from 1 to count of theRGBValues
		set theCurrentRGBValue to (item a of theRGBValues) --  div 256   for 65235 based values
		if theCurrentRGBValue is 256 then set theCurrentRGBValue to 255
		set theFirstItem to item ((theCurrentRGBValue div 16) + 1) of theHexList
		set theSecondItem to item (((theCurrentRGBValue / 16 mod 1) * 16) + 1) of theHexList
		set theHexValue to (theHexValue & theFirstItem & theSecondItem) as string
	end repeat
	return ("#" & theHexValue) as string
end convertRGBColorToHexValue

Yes.

The problem with that approach is that you can’t really pass an integer because of bridging issues. You can work around it with a bit of hacky code:

	((current application's NSString's stringWithFormat_("%04Xz%04Xz%04Xz", (item 1 of rgbList) / 256, (item 2 of rgbList) / 256, (item 3 of rgbList) / 256))'s stringByReplacingOccurrencesOfString:"57z" withString:"") as text

But it’s quite slow — it takes more than 0.01 seconds. Whereas the non-ASObjC method is more than 200 times faster. No contest, even if @NigelGarvey thinks we should be more patient :slight_smile: See post below.

On my system, your AppleScriptObjC script takes not time and returns the right Hex number after a tiny modification. (I dont know why it adds the string “27z” and not “57z”. is it a typo?).

((current application's NSString's stringWithFormat_("%04Xz%04Xz%04Xz", (item 1 of rgbList) div 256, (item 2 of rgbList) div 256, (item 3 of rgbList) div 256))'s stringByReplacingOccurrencesOfString:"27z" withString:"") as text

Here are the results in Script Geek:


Even if Vanilla is 5x faster, AppleScriptObjC is under 0.001 second!
It’s fast enough for me!
:wink:

After some tests, I figured out why 27z:
It’s because the passed numbers where integers.
57z it’s when they are reals.

(Of course, I know you know that :wink: )

Or perhaps a snap-together assembly from pre-existing parts, as if from a Lego bin ?

-- hexFromRGB :: (Int, Int, Int) -> String
on hexFromRGB(rgb)
    "#" & intercalateString("", map(my showHex, rgb))
end hexFromRGB


-- TEST ------------------------------------------------------------------
on run
    hexFromRGB({238, 115, 117})
    
    --> "#EE7375"
end run


-- GENERIC FUNCTIONS ------------------------------------------------

-- intercalateString :: String -> [String] -> String
on intercalateString(s, xs)
    set {dlm, text item delimiters} to {text item delimiters, s}
    set str to xs as text
    set text item delimiters to dlm
    str
end intercalateString

-- intToDigit :: Int -> Char
on intToDigit(n)
    if n ≥ 0 and n < 16 then
        item (n + 1) of "0123456789ABCDEF"
    else
        "?"
    end if
end intToDigit

-- showHex :: Int -> String
on showHex(n)
    showIntAtBase(16, my intToDigit, n, "")
end showHex

-- showIntAtBase :: Int -> (Int -> Char) -> Int -> String -> String
on showIntAtBase(base, toChr, n, rs)
    script go
        property fChar : mReturn(toChr)'s |λ|
        on |λ|(tpl, r)
            set {n, d} to tpl
            set r_ to fChar(d) & r
            if n ≠ 0 then
                |λ|(quotRem(n, base), r_)
            else
                r_
            end if
        end |λ|
    end script
    
    if base ≤ 1 then
        "error: showIntAtBase applied to unsupported base"
    else if n < 0 then
        "error: showIntAtBase applied to negative number"
    else
        go's |λ|(quotRem(n, base), rs)
    end if
end showIntAtBase

-- quotRem :: Int -> Int -> (Int, Int)
on quotRem(m, n)
    {m div n, m mod n}
end quotRem

-- map :: (a -> b) -> [a] -> [b]
on map(f, xs)
    tell mReturn(f)
        set lng to length of xs
        set lst to {}
        repeat with i from 1 to lng
            set end of lst to |λ|(item i of xs, i, xs)
        end repeat
        return lst
    end tell
end map

-- Lift 2nd class handler function into 1st class script wrapper
-- mReturn :: First-class m => (a -> b) -> m (a -> b)
on mReturn(f)
    if class of f is script then
        f
    else
        script
            property |λ| : f
        end script
    end if
end mReturn

-- unwords :: [String] -> String
on unwords(xs)
    intercalateString(space, xs)
end unwords

Can I ask why you’d want to modify the docx contents from the XML?

@ComplexPoint
Even if I find your scripts very interesting, they are to much… complex for me.
I’m just a graphic designer whose developing some tools for my private use.
But I’m sure some others will get benefit from your participation!

@suzume
It’s not the Word document I want to modify.
It’s the color theme file located in the Applications Support system folder.

2 Likes

too … complex for me

I know what you mean :slight_smile:

On the other hand, when a smallish set of general prefabricated units is available, snapping them together is possibly a bit simpler than starting from scratch. All we have to write is:

-- hexFromRGB :: (Int, Int, Int) -> String
on hexFromRGB(rgb)
    "#" & intercalateString("", map(my showHex, rgb))
end hexFromRGB

What version of the OS are you running? I wonder why we get such a difference.

Answering my own question: I also had Console.app running. Quitting it resulted in speeds more like @ionah’s (and more like I expected).

At least one of your blocks isn’t very child-proof:

   set text item delimiters to linefeed -- for example, as might be the case in the real world
   hexFromRGB({238, 115, 117})
1 Like

That’ll teach me to hand-roll on the fly :slight_smile:

(adjusted above – thanks)

I thought that out of deference to delicate sensibilities, I should avoid an intercalate built to be polymorphic (see below), but composition and fix time might have been saved if I had just stuck with reaching into the Lego bin …

-- intercalate :: [a] -> [[a]] -> [a]
-- intercalate :: String -> [String] -> String
on intercalate(sep, xs)
    concat(intersperse(sep, xs))
end intercalate

-- concat :: [[a]] -> [a]
-- concat :: [String] -> String
on concat(xs)
    if length of xs > 0 and class of (item 1 of xs) is string then
        set acc to ""
    else
        set acc to {}
    end if
    repeat with i from 1 to length of xs
        set acc to acc & item i of xs
    end repeat
    acc
end concat

-- intersperse(0, [1,2,3]) -> [1, 0, 2, 0, 3]
-- intersperse :: Char -> String -> String
-- intersperse :: a -> [a] -> [a]
on intersperse(sep, xs)
    set lng to length of xs
    if lng > 1 then
        set acc to {item 1 of xs}
        repeat with i from 2 to lng
            set acc to acc & {sep, item i of xs}
        end repeat
        if class of xs is string then
            concat(acc)
        else
            acc
        end if
    else
        xs
    end if
end intersperse

Thank you shane for reporting, that comforts me in adding this code in one of my libraries.

I’d be happier if you used something like this:

on hexFromRGB(rgbList)
	set theResult to ""
	repeat with aValue in rgbList
		set theString to current application's NSString's stringWithFormat_("%04Xz", aValue)
		set theResult to theResult & (theString's substringToIndex:2) as text
	end repeat
	return theResult
end hexFromRGB

It doesn’t feel quite so dirty.

Or perhaps:

on hexFromRGB(rgbList)
	return text 1 thru 6 of ((current application's NSString's stringWithFormat_("%08X", (beginning of rgbList) div 256 * 65536 + (item 2 of rgbList) div 256 * 256 + (end of rgbList) div 256)) as text)
end hexFromRGB

BTW, Shane. Your handler doesn’t alway return the right results. eg.:

hexFromRGB({800, 255, 65535})
--> "32FFFF" (should be "0300FF")

so dirty

Hahaha – curious how symmetrical these aesthetic responses are :slight_smile:

It’s expecting values from 0-255. I’m not sure why I changed mid-thread — I think it was to match @ionah and @ComplexPoint’s versions that were based on 0-255 values. Although looking back there was a bit of jumping around.

Hex values are for sRGB colors.
If you pass a color described with 65536-based values, they can’t be less than 256 and they can’t be decimals.
{800, 255, 65535} is not compatible with the sRGB color space.

use framework "Foundation"
use framework "AppKit"
use scripting additions

on hexFromRGB:rgbList
	set theResult to ""
	repeat with aValue in rgbList
		if aValue > 255 then set aValue to (aValue div 256)
		set theString to current application's NSString's stringWithFormat_("%04Xz", aValue)
		set theResult to theResult & (theString's substringToIndex:2) as text
	end repeat
	return theResult
end hexFromRGB:

set result1 to my hexFromRGB:{65535, 29952, 29440}
set result2 to my hexFromRGB:{255, 117, 115}
result1 & " - " & result2