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


(Jonas Whale) #1

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:


(Shane Stanley) #2

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.


(Jonas Whale) #3

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

(Shane Stanley) #4

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.


(Jonas Whale) #5

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:


(Jonas Whale) #6

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: )


#7

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

(Jean Christophe Helary) #8

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


(Jonas Whale) #9

@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.


#10

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

(Shane Stanley) #11

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


(Shane Stanley) #12

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


(Shane Stanley) #13

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})

#14

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

(Jonas Whale) #15

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


(Shane Stanley) #16

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.


(Nigel Garvey) #17

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")

#18

so dirty

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


(Shane Stanley) #19

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.


(Jonas Whale) #20

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