Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Convert embed images to linked

I'm trying to fix the macro, shown below.

It is intended to convert embed images to linked (via IncludePicture). However, in it's current state, images are added at the bottom of the document. Obviously, it's far from being perfect. Instead, macro should replace embed images with the linked ones, one by one, like shown here:

enter image description here

How to fix it?

Also, note: Macro should be launched from another file. So, you need two documents: one with macro and one with images. It's not good, but it's how it works currently.

Code:

Sub MakeDocMediaLinked()
    Application.ScreenUpdating = False
    Dim StrOutFold As String, Obj_App As Object, Doc As Document, Rng As Range
    Dim StrDocFile As String, StrZipFile As String, StrMediaFile As String
    With Application.Dialogs(wdDialogFileOpen)
        If .Show = -1 Then
            .Update
            Set Doc = ActiveDocument
        End If
    End With
    If Doc Is Nothing Then Exit Sub
    With Doc
        ' ID the document to process
        StrDocFile = .FullName
        StrOutFold = Split(StrDocFile, ".")(0) & "_Media"
        .Close SaveChanges:=False
    End With
    ' Test for existing output folder, create it if it doesn't already exist
    If Dir(StrOutFold, vbDirectory) = "" Then MkDir StrOutFold
    ' In case the output folder is not empty. Also, in case the file has no media
    On Error Resume Next
    ' Delete any files in the output folder
    Kill StrOutFold & "\*.*"
    ' Create a Shell App for accessing the zip archives
    Set Obj_App = CreateObject("Shell.Application")
    ' Define the zip name
    StrZipFile = Split(StrDocFile, ".")(0) & ".zip"
    ' Create the zip file, by simply copying to a new file with a zip extension
    FileCopy StrDocFile, StrZipFile
    ' Extract the zip archive's media files to the temporary folder
    Obj_App.NameSpace(StrOutFold & "\").CopyHere Obj_App.NameSpace(StrZipFile & "\word\media\").Items
    ' Delete the zip file - the loop takes care of timing issues
    Do While Dir(StrZipFile) <> ""
        Kill StrZipFile
    Loop
    ' Restore error trapping
    On Error GoTo 0
    ' Get the temporary folder's file listing
    StrMediaFile = Dir(StrOutFold & "\*.*", vbNormal)
    Documents.Open FileName:=StrDocFile
    With ActiveDocument
        ' Process the temporary folder's files
        While StrMediaFile <> ""
            .Range.InsertAfter vbCr
            Set Rng = .Paragraphs.Last.Range
            .Fields.Add Range:=Rng, Type:=wdFieldEmpty, PreserveFormatting:=False, _
                Text:="INCLUDEPICTURE """ & Replace(StrOutFold & "\" & StrMediaFile, "\", "\\") & """ \d"
            ' Get the next media file
            StrMediaFile = Dir()
        Wend
        .Fields.Update
    End With
    Application.ScreenUpdating = True
End Sub
like image 852
john c. j. Avatar asked Sep 26 '17 21:09

john c. j.


2 Answers

You could also parse the XML returned by Document.Content.XML to extract all the images. Then update each source with the path of the external image and write back the XML with Document.Content.InsertXML.

Writing the XML back automatically adds a linked field which seem to be one of your requirement. It's faster that working with the clipboard and it doesn't alter the style of the shape. Though, you might need to tweak the code to handle specific cases.

Private Declare PtrSafe Function CryptStringToBinaryW Lib "Crypt32" (ByVal pszString As LongPtr, ByVal cchString As Long, ByVal dwFlags As Long, ByRef pbBinary As Byte, ByRef cbBinary As Long, ByVal pdwSkip As LongPtr, ByVal pdwFlags As LongPtr) As Boolean

Public Sub Example()
  SaveAslinkedImages ActiveDocument, "c:\temp\myfile-no-img.docx"
End Sub

Public Sub SaveAslinkedImages(Doc As Document, fname As String)
  Dim objXml As Object, binData As Object, binName$, nodes, node
  Dim imgPath$, docDir$, imgDir$, i&, data() As Byte

  Set objXml = VBA.CreateObject("Msxml2.DOMDocument.6.0")
  objXml.Async = False
  objXml.validateOnparse = False

  ' parse xml document '
  objXml.LoadXML Doc.Content.XML

  ' add namespaces for SelectNodes '
  objXml.setProperty "SelectionNamespaces", _
    objXml.DocumentElement.getAttributeNode("xmlns:w").XML & " " & _
    objXml.DocumentElement.getAttributeNode("xmlns:v").XML

  ' create the  media folder '
  docDir = Left(fname, InStrRev(fname, "\") - 1)
  imgDir = Left(fname, InStrRev(fname, ".") - 1) & "_media"
  MakeDir imgDir

  ' iterate each image data '
  For Each binData In objXml.SelectNodes("//w:binData")
    binName = binData.getAttribute("w:name")

    ' get all the nodes referencing the image data '
    Set nodes = objXml.SelectNodes("//v:imagedata[@src='" & binName & "']")

    If nodes.Length Then ' if any '
      ' build image path '
      imgPath = imgDir & "\" & Mid(binName, InStrRev(binName, "/") + 1)

      ' save base64 data to file '
      DecodeBase64 binData.Text, data
      SaveBytesAs data, imgPath

      ' remove the data '
      binData.ParentNode.RemoveChild binData

      ' for each image '
      For Each node In nodes
        ' set id '
        node.ParentNode.setAttribute "id", node.ParentNode.getAttribute("o:spid")

        ' remove o namespace '
        node.ParentNode.Removeattribute "o:spid"
        node.Removeattribute "o:title"

        ' set external image source '
        node.setAttribute "src", imgPath
      Next
    End If
  Next

  ' write back the xml and save the document '
  Doc.Content.InsertXML objXml.XML
  Doc.SaveAs2 fname

End Sub

Public Sub SaveBytesAs(data() As Byte, path As String)
  Open path For Binary Access Write As #5
  Put #5, 1, data
  Close #5
End Sub

Public Sub MakeDir(path As String)
  If Len(Dir(path, vbDirectory)) Then Exit Sub
  MakeDir Left(path, InStrRev(path, "\") - 1)
  MkDir path
End Sub

Public Function DecodeBase64(str As String, out() As Byte) As Boolean
  Dim size As Long
  size = ((Len(str) + 3) \ 4) * 3
  ReDim out(0 To size - 1) As Byte
  DecodeBase64 = CryptStringToBinaryW(StrPtr(str), Len(str), 1, out(0), size, 0, 0)
  If size - 1 < UBound(out) Then ReDim Preserve out(0 To size - 1)
End Function
like image 179
Florent B. Avatar answered Sep 23 '22 23:09

Florent B.


This is where your code is going astray:-

With ActiveDocument
            .Range.InsertAfter vbCr
            Set Rng = .Paragraphs.Last.Range

You are inserting a carriage return at the end of the document (which actually inserts a new blank paragraph) and then add a field in that paragraph. Obviously, you want the field somewhere else.

Meanwhile, if you wish to delete the links you should let your code do that. I haven't been able to figure out whether your code makes an attempt in that direction but presume that it extracts the picture's path from the link. So, the link should be located and deleted after giving up its path, and the field inserted in its place.

like image 43
Variatus Avatar answered Sep 20 '22 23:09

Variatus