; AutoHotkey v2 script - Catalogue Documentaire (minimal functional scaffold) #Requires AutoHotkey v2.0 #Warn global AppTitle := "Catalogue documentaire - CETI - opérations fiscales" global DataFile := A_ScriptDir "\catalogue-data.txt" global ImagesDir := A_ScriptDir "\images" global ThumbnailsDir := ImagesDir "\previews" global ListsDir := A_ScriptDir "\lists" global CounterFile := A_ScriptDir "\counter.txt" global SavesDir := A_ScriptDir "\saves" global AppVersion := "1.0.0" global AppVersionDate := "2025-10-02" ; Data structures global Documents := [] ; Array of document objects (Map) global Gouvernements := [] global ClassesLot := [] global DocumentsKofax := [] global CurrentSelectedId := "" ; GUI refs global MainGui := Gui("+Resize +MaximizeBox", AppTitle) global LV, Pic, PicName, SearchEdit, DDGov, DDClasse, DDKofax, PreviewFrame, PreviewHeader global DetailFrame, DetailTitleLabel, DetailTitleText, DetailTagLabel, DetailTagText global DetailClasseLabel, DetailClasseText, DetailGovLabel, DetailGovText global DetailKofaxLabel, DetailKofaxText, DetailDirectivesLabel, DetailDirectivesEdit global PreviewImageOriginalW := 0, PreviewImageOriginalH := 0 global PreviewCurrentPath := "" global PreviewOriginalPath := "" global PreviewBitmap := 0 global FullImageGui := 0 global FullImagePic := 0 global FullImageBitmap := 0 global FullImageOrigW := 0 global FullImageOrigH := 0 global FullImageScale := 1 global FullImageIsDragging := false global FullImageDragStartX := 0, FullImageDragStartY := 0 global FullImageDragStartOffX := 0, FullImageDragStartOffY := 0 global FullImageLastRenderedScale := 0 ; Resize optimization: debounce timers global MainResizeTimer := 0 global FullImageResizeTimer := 0 LoadLists() LoadData() CheckAndBackup() ; Backup automatique au démarrage CreateMainGui() MainGui.Show("w1800 h900") return CreateMainGui() { global MainGui, LV, Pic, PicName, SearchEdit, DDGov, DDClasse, DDKofax, PreviewFrame, PreviewHeader global DetailFrame, DetailTitleLabel, DetailTitleText, DetailTagLabel, DetailTagText global DetailClasseLabel, DetailClasseText, DetailGovLabel, DetailGovText global DetailKofaxLabel, DetailKofaxText, DetailDirectivesLabel, DetailDirectivesEdit MainGui.MarginX := 10, MainGui.MarginY := 10 ; Search row MainGui.Add("Text", "x10 y10", "Recherche:") SearchEdit := MainGui.Add("Edit", "x80 y8 w300 h20") SearchEdit.OnEvent("Change", ApplyFilters) MainGui.Add("Button", "x390 y7 w80 h22", "Effacer").OnEvent("Click", ClearFilters) ; Filters row MainGui.Add("Text", "x10 y40", "Gouvernement:") govItems := ["Tous"] if (Gouvernements.Length) govItems.Push(Gouvernements*) DDGov := MainGui.Add("DropDownList", "x95 y38 w150 h250", govItems) DDGov.Value := 1 DDGov.OnEvent("Change", ApplyFilters) MainGui.Add("Text", "x255 y40", "Classe de lot:") classeItems := ["Tous"] if (ClassesLot.Length) classeItems.Push(ClassesLot*) DDClasse := MainGui.Add("DropDownList", "x325 y38 w150 h250", classeItems) DDClasse.Value := 1 DDClasse.OnEvent("Change", ApplyFilters) MainGui.Add("Text", "x485 y40", "Document Kofax:") ; Build unique list of all Kofax documents from all classes kofaxItems := ["Tous"] kofaxMap := Map() global KofaxPerClass if (!IsSet(KofaxPerClass)) KofaxPerClass := ReadKofaxPerClass() for classe, docs in KofaxPerClass { for doc in docs { if (!kofaxMap.Has(doc)) kofaxMap[doc] := true } } for doc in kofaxMap kofaxItems.Push(doc) DDKofax := MainGui.Add("DropDownList", "x585 y38 w200 h250", kofaxItems) DDKofax.Value := 1 DDKofax.OnEvent("Change", ApplyFilters) ; Actions row MainGui.Add("Button", "x800 y37 w80 h22", "Nouveau").OnEvent("Click", NewDocument) MainGui.Add("Button", "x890 y37 w80 h22", "Modifier").OnEvent("Click", EditDocument) MainGui.Add("Button", "x980 y37 w80 h22", "Supprimer").OnEvent("Click", DeleteDocument) MainGui.Add("Button", "x1070 y37 w100 h22", "Ajouter modèle").OnEvent("Click", AddModel) MainGui.Add("Button", "x1180 y37 w100 h22", "Gérer les listes").OnEvent("Click", ManageLists) MainGui.Add("Button", "x1290 y37 w80 h22", "Infos").OnEvent("Click", ShowInfoPopup) ; ListView at top LV := MainGui.Add("ListView", "x10 y70 w520 h400", ["ID","Titre","Tag","Modèle","Classe de lot","Gouvernement","Document Kofax"]) LV.OnEvent("ItemSelect", OnListSelect) LV.OnEvent("DoubleClick", (*) => EditDocument()) ; Set column widths to auto-adjust based on content and header LV.ModifyCol(1, "AutoHdr") ; ID LV.ModifyCol(2, "AutoHdr") ; Titre LV.ModifyCol(3, "AutoHdr") ; Tag LV.ModifyCol(4, "AutoHdr") ; Modèle LV.ModifyCol(5, "AutoHdr") ; Classe de lot LV.ModifyCol(6, "AutoHdr") ; Gouvernement LV.ModifyCol(7, "AutoHdr") ; Document Kofax ; Detail section below ListView DetailFrame := MainGui.Add("GroupBox", "x10 y480 w520 h250", "Détails du document") DetailTitleLabel := MainGui.Add("Text", "x20 y505 w60 h18", "Titre:") DetailTitleText := MainGui.Add("Edit", "x85 y503 w430 h24 +ReadOnly Background0xF0F0F0", "") DetailTitleText.SetFont("s10 bold", "Segoe UI") DetailTagLabel := MainGui.Add("Text", "x20 y535 w60 h18", "Tag:") DetailTagText := MainGui.Add("Edit", "x85 y533 w100 h20 +ReadOnly Background0xF0F0F0", "") DetailTagText.SetFont("s8 bold", "Segoe UI") DetailGovLabel := MainGui.Add("Text", "x200 y535 w100 h18", "Gouvernement:") DetailGovText := MainGui.Add("Edit", "x305 y533 w210 h20 +ReadOnly Background0xF0F0F0", "") DetailGovText.SetFont("s8 bold", "Segoe UI") DetailClasseLabel := MainGui.Add("Text", "x20 y561 w80 h18", "Classe de lot:") DetailClasseText := MainGui.Add("Edit", "x105 y559 w410 h20 +ReadOnly Background0xF0F0F0", "") DetailClasseText.SetFont("s8 bold", "Segoe UI") DetailKofaxLabel := MainGui.Add("Text", "x20 y587 w120 h18", "Document Kofax:") DetailKofaxText := MainGui.Add("Edit", "x145 y585 w370 h20 +ReadOnly Background0xF0F0F0", "") DetailKofaxText.SetFont("s8 bold", "Segoe UI") DetailDirectivesLabel := MainGui.Add("Text", "x20 y613 w120 h18", "Directives:") DetailDirectivesEdit := MainGui.Add("Edit", "x20 y633 w495 h85 +Multi +ReadOnly +VScroll Background0xF0F0F0", "") DetailDirectivesEdit.SetFont("s8 bold cA00000", "Segoe UI") PreviewHeader := MainGui.Add("Text", "x540 y70 w300 h20 Center", "Aperçu de l'image") PreviewFrame := MainGui.Add("Text", "x540 y90 w300 h350 Border") Pic := MainGui.Add("Picture", "x540 y90 w300 h350") PicName := MainGui.Add("Text", "x540 y450 w300 h20 Center") Pic.OnEvent("Click", ShowFullImage) ; Resize handler MainGui.OnEvent("Size", OnMainResize) ; Close handler for backup on exit MainGui.OnEvent("Close", OnMainGuiClose) PopulateListView(Documents) ; Initial layout pass to avoid overlap MainGui.GetClientPos(, , &cw, &ch) DoMainResize(cw, ch) } OnMainResize(gui, minMax, w, h) { global LV, Pic, PicName, PreviewFrame, MainResizeTimer if (minMax = -1) ; minimized return ; Debounce: delay layout recalculation until resize stops if (MainResizeTimer) SetTimer(MainResizeTimer, 0) ; cancel previous MainResizeTimer := () => DoMainResize(w, h) SetTimer(MainResizeTimer, -50) ; execute once after 50ms } ShowInfoPopup(*) { global AppVersion, AppVersionDate g := Gui("+AlwaysOnTop +ToolWindow", "Informations") g.MarginX := 15 g.MarginY := 15 info := " ( Catalogue documentaire pour organiser et consulter les documents numérisés pour le département CETI - opérations fiscales. - Recherche instantanée par mot-clé dans la barre en haut. - Filtres par gouvernement, classe de lot et document Kofax (listes déroulantes). - La liste et le panneau de détails sont synchronisés : double-clic pour modifier un élément. - Aperçu d'image à droite avec ouverture plein écran au clic. - Bouton 'Gérer les listes' pour mettre à jour les valeurs (gouvernements, classes, Kofax). - Sauvegarde automatique : une sauvegarde est créée à l'ouverture/fermeture si > 1 h depuis la dernière ; les 10 dernières versions sont conservées dans le dossier 'saves'. - Données stockées dans 'catalogue-data.txt' (format balisé ##allobob##) pour préserver les retours de ligne. - Images originales dans 'images', prévisualisations dans 'images\\previews'. )" g.Add("Edit", "x15 y15 w500 h160 +ReadOnly +Multi +VScroll", info) g.Add("Text", "x15 y185 w500 h20", "Version: " AppVersion) g.Add("Text", "x15 y210 w500 h20", "Date de version: " AppVersionDate) g.Add("Text", "x15 y235 w500 h20", "Programmé par: Etienne Langlois Robitaille 2025") g.Add("Button", "x220 y265 w80 h24", "OK").OnEvent("Click", (*) => g.Destroy()) g.Show("w540 h310") } DoMainResize(w, h) { global LV, Pic, PicName, PreviewFrame, PreviewHeader, MainGui global DetailFrame, DetailTitleLabel, DetailTitleText, DetailTagLabel, DetailTagText global DetailClasseLabel, DetailClasseText, DetailGovLabel, DetailGovText global DetailKofaxLabel, DetailKofaxText, DetailDirectivesLabel, DetailDirectivesEdit ; Suspend redraw for smoother resize SendMessage(0x000B, 0, 0, LV.Hwnd) ; WM_SETREDRAW off ; Calculate available space - 50/50 split between left and right margin := 10 availW := w - margin*3 leftW := Floor(availW * 0.5) rightW := availW - leftW ; Calculate heights: split left side 60% ListView / 40% Detail startY := 70 totalLeftH := h - startY - 20 if (totalLeftH < 200) totalLeftH := 200 detailH := Max(250, Floor(totalLeftH * 0.4)) listH := totalLeftH - detailH - margin if (listH < 120) listH := 120 ; ListView at top listY := startY LV.Move(margin, listY, leftW, listH) ; Detail section below ListView detailY := listY + listH + margin DetailFrame.Move(margin, detailY, leftW, detailH) ; Position detail controls relative to DetailFrame with proper spacing offsetY := detailY + 25 labelX := margin + 10 valueX := labelX + 65 labelCol2X := margin + 190 valueCol2X := labelCol2X + 95 ; Row 1: Titre (bold, slightly smaller) DetailTitleLabel.Move(labelX, offsetY, 60, 18) DetailTitleText.Move(valueX, offsetY - 2, leftW - (valueX - margin) - 15, 24) offsetY += 30 ; Row 2: Tag and Gouvernement (bold) DetailTagLabel.Move(labelX, offsetY, 60, 18) DetailTagText.Move(valueX, offsetY - 2, 100, 20) DetailGovLabel.Move(labelCol2X, offsetY, 110, 18) DetailGovText.Move(labelCol2X + 110, offsetY - 2, leftW - (labelCol2X + 110 - margin) - 15, 20) offsetY += 26 ; Row 3: Classe Lot (bold) DetailClasseLabel.Move(labelX, offsetY, 85, 18) DetailClasseText.Move(labelX + 85, offsetY - 2, leftW - (labelX + 85 - margin) - 15, 20) offsetY += 26 ; Row 4: Document Kofax (bold) DetailKofaxLabel.Move(labelX, offsetY, 120, 18) DetailKofaxText.Move(valueX + 60, offsetY - 2, leftW - (valueX + 60 - margin) - 15, 20) offsetY += 26 ; Row 5: Directives (multiline, bold, dark red) DetailDirectivesLabel.Move(labelX, offsetY, 120, 18) offsetY += 20 directivesH := detailY + detailH - offsetY - 10 if (directivesH < 30) directivesH := 30 DetailDirectivesEdit.Move(labelX, offsetY, leftW - (labelX - margin) - 15, directivesH) ; Preview position and size (full height on right side) prevX := margin + leftW + margin prevH := totalLeftH - 40 if (prevH < 100) prevH := 100 if (IsObject(PreviewHeader)) PreviewHeader.Move(prevX, startY, rightW, 20) PreviewFrame.Move(prevX, startY + 20, rightW, prevH) UpdatePreviewLayout() PicName.Move(prevX, startY + 20 + prevH + 5, rightW, 20) ; Re-enable redraw and force repaint SendMessage(0x000B, 1, 0, LV.Hwnd) ; WM_SETREDRAW on DllCall("RedrawWindow", "Ptr", MainGui.Hwnd, "Ptr", 0, "Ptr", 0, "UInt", 0x0081) ; RDW_INVALIDATE | RDW_UPDATENOW } ClearFilters(*) { global SearchEdit, DDGov, DDClasse, DDKofax SearchEdit.Value := "" DDGov.Value := 1 ; "Tous" DDClasse.Value := 1 ; "Tous" DDKofax.Value := 1 ; "Tous" ApplyFilters() } ApplyFilters(*) { global Documents, SearchEdit, DDGov, DDClasse, DDKofax search := Trim(SearchEdit.Value) gov := DDGov.Text classe := DDClasse.Text kofax := DDKofax.Text filtered := [] matchedTitles := Map() ; Track which titles have matches ; First pass: find all matching titles for doc in Documents { if (gov != "Tous" && doc.Gouvernement != gov) continue if (classe != "Tous" && doc.ClasseLot != classe) continue if (kofax != "Tous" && doc.DocumentKofax != kofax) continue if (search) { text := doc.Titre " " doc.Tag " " doc.ClasseLot " " doc.Gouvernement " " doc.DocumentKofax " " doc.Keywords if !InStr(text, search) continue } matchedTitles[doc.Titre] := true } ; Second pass: add ALL models of matched titles for doc in Documents { if (matchedTitles.Has(doc.Titre)) { filtered.Push(doc) } } PopulateListView(filtered) } PopulateListView(list) { global LV LV.Delete() for doc in list { modelNum := GetModelNumber(doc) LV.Add("", doc.Id, doc.Titre, doc.Tag, modelNum, doc.ClasseLot, doc.Gouvernement, doc.DocumentKofax) } ; Auto-adjust all column widths based on content and header Loop 7 { LV.ModifyCol(A_Index, "AutoHdr") } if (LV.GetCount() > 0) { LV.Modify(1, "Select Vis") ; Manually trigger preview and detail update id := LV.GetText(1, 1) ShowPreviewById(id) UpdateDetailSection(id) global CurrentSelectedId := id } else { ShowPreview("") UpdateDetailSection("") global CurrentSelectedId := "" } } OnListSelect(ctrl, item, selected) { if (!selected) return global LV id := LV.GetText(item, 1) ShowPreviewById(id) UpdateDetailSection(id) global CurrentSelectedId := id } ShowPreviewById(id) { global Documents for doc in Documents { if (doc.Id = id) { ShowPreview(doc.Filename) return } } ShowPreview("") } UpdateDetailSection(id) { global Documents, DetailTitleText, DetailTagText, DetailClasseText, DetailGovText, DetailKofaxText, DetailDirectivesEdit ; Clear all fields first DetailTitleText.Value := "" DetailTagText.Value := "" DetailClasseText.Value := "" DetailGovText.Value := "" DetailKofaxText.Value := "" DetailDirectivesEdit.Value := "" if (!id) return ; Find the document and populate fields for doc in Documents { if (doc.Id = id) { DetailTitleText.Value := doc.Titre ? doc.Titre : "" DetailTagText.Value := doc.Tag ? doc.Tag : "" DetailClasseText.Value := doc.ClasseLot ? doc.ClasseLot : "" DetailGovText.Value := doc.Gouvernement ? doc.Gouvernement : "" DetailKofaxText.Value := doc.DocumentKofax ? doc.DocumentKofax : "" DetailDirectivesEdit.Value := doc.Directives ? doc.Directives : "" return } } } ReleasePreviewBitmap() { global PreviewBitmap if (PreviewBitmap) { DllCall("DeleteObject", "Ptr", PreviewBitmap) PreviewBitmap := 0 } } ReleaseFullImageBitmap() { global FullImageBitmap if (FullImageBitmap) { DllCall("DeleteObject", "Ptr", FullImageBitmap) FullImageBitmap := 0 } } ShowPreview(filename) { global Pic, PicName, ImagesDir, PreviewImageOriginalW, PreviewImageOriginalH global PreviewCurrentPath, PreviewOriginalPath, FullImageOrigW, FullImageOrigH, ThumbnailsDir ReleasePreviewBitmap() PreviewCurrentPath := "" PreviewOriginalPath := "" if (!filename) { Pic.Value := "" PicName.Value := "" PreviewImageOriginalW := 0 PreviewImageOriginalH := 0 UpdatePreviewLayout() return } path := ImagesDir "\" filename ; Previews are saved as JPG with the same base name SplitPath filename, , , , &base previewPathJpg := ThumbnailsDir "\" base ".jpg" previewPathPng := ThumbnailsDir "\" base ".png" previewPathSame := ThumbnailsDir "\" filename if FileExist(path) { ; Store original path for full-size view PreviewOriginalPath := path ; Get original dimensions from the full-size image origDims := GetImageDimensions(path) FullImageOrigW := origDims["w"] FullImageOrigH := origDims["h"] ; Prefer preview for fast loading in pane (try .jpg, then .png, then same name) fastPath := FileExist(previewPathJpg) ? previewPathJpg : (FileExist(previewPathPng) ? previewPathPng : (FileExist(previewPathSame) ? previewPathSame : path)) dims := GetImageDimensions(fastPath) PreviewImageOriginalW := dims["w"] PreviewImageOriginalH := dims["h"] PicName.Value := filename PreviewCurrentPath := fastPath UpdatePreviewLayout() } else { Pic.Value := "" PicName.Value := "" PreviewImageOriginalW := 0 PreviewImageOriginalH := 0 UpdatePreviewLayout() } } UpdatePreviewLayout() { global PreviewFrame, Pic, PreviewImageOriginalW, PreviewImageOriginalH global PreviewCurrentPath, PreviewBitmap if (!IsObject(PreviewFrame)) return PreviewFrame.GetPos(&frameX, &frameY, &frameW, &frameH) if (frameW <= 0 || frameH <= 0) return if (PreviewImageOriginalW <= 0 || PreviewImageOriginalH <= 0 || !PreviewCurrentPath) { ReleasePreviewBitmap() Pic.Value := "" return } scaleW := frameW / PreviewImageOriginalW scaleH := frameH / PreviewImageOriginalH scale := Min(scaleW, scaleH, 1) targetW := Max(1, Round(PreviewImageOriginalW * scale)) targetH := Max(1, Round(PreviewImageOriginalH * scale)) offsetX := frameX + Floor((frameW - targetW) / 2) offsetY := frameY + Floor((frameH - targetH) / 2) ; Clear previous content and erase background to avoid ghosting Pic.Value := "" ; Force erase of the preview background area before drawing new image try DllCall("RedrawWindow", "Ptr", PreviewFrame.Hwnd, "Ptr", 0, "Ptr", 0, "UInt", 0x0185) ; RDW_INVALIDATE|RDW_ERASE|RDW_ALLCHILDREN|RDW_UPDATENOW Pic.Move(offsetX, offsetY, targetW, targetH) ReleasePreviewBitmap() ; Validate path exists before loading if (!FileExist(PreviewCurrentPath)) { Pic.Value := "" return } ; Load scaled bitmap (restore -Resize for reliability) options := "W" targetW " H" targetH " -Resize" try { hBmp := LoadPicture(PreviewCurrentPath, options) if (hBmp) { PreviewBitmap := hBmp Pic.Value := "HBITMAP:" PreviewBitmap } else { PreviewBitmap := 0 Pic.Value := PreviewCurrentPath } } catch { PreviewBitmap := 0 Pic.Value := "" } ; Force repaint of picture control try DllCall("InvalidateRect", "Ptr", Pic.Hwnd, "Ptr", 0, "Int", 1) try DllCall("RedrawWindow", "Ptr", Pic.Hwnd, "Ptr", 0, "Ptr", 0, "UInt", 0x0081) } GetImageDimensions(path) { dims := Map("w", 0, "h", 0) if !FileExist(path) return dims try { img := ComObject("WIA.ImageFile") img.LoadFile(path) dims["w"] := img.Width dims["h"] := img.Height return dims } catch { } hBitmap := LoadPicture(path) if (hBitmap) { info := Buffer(32, 0) DllCall("GetObjectW", "Ptr", hBitmap, "Int", info.Size, "Ptr", info) dims["w"] := NumGet(info, 4, "Int") dims["h"] := NumGet(info, 8, "Int") DllCall("DeleteObject", "Ptr", hBitmap) } return dims } GetCurrentWorkArea(&waL, &waT, &waR, &waB) { ; Determine the monitor work area under the mouse cursor; fallback to primary screen try { MouseGetPos(&mx, &my) count := MonitorGetCount() found := 0 Loop count { i := A_Index MonitorGet(i, &l, &t, &r, &b) if (mx >= l && mx < r && my >= t && my < b) { found := i break } } if (!found) found := 1 MonitorGetWorkArea(found, &waL, &waT, &waR, &waB) } catch { ; Fallback: use primary screen bounds waL := 0, waT := 0, waR := A_ScreenWidth, waB := A_ScreenHeight } } ShowFullImage(*) { global PreviewOriginalPath, FullImageGui, FullImagePic, FullImageBitmap, FullImageOrigW, FullImageOrigH global FullImageHScroll, FullImageVScroll global FullImageScale, FullImageIsDragging, FullImageDragStartX, FullImageDragStartY global FullImageDragStartOffX, FullImageDragStartOffY, FullImageLastRenderedScale if (!PreviewOriginalPath) return if (FullImageGui) { try FullImageGui.Destroy() FullImageGui := 0 } FullImageGui := Gui("+Resize +MaximizeBox +MinimizeBox", "Image plein format") FullImageGui.MarginX := 0 FullImageGui.MarginY := 0 FullImagePic := FullImageGui.Add("Picture", "x0 y0") ReleaseFullImageBitmap() bmp := LoadPicture(PreviewOriginalPath) if (bmp) { FullImageBitmap := bmp FullImagePic.Value := "HBITMAP:" FullImageBitmap } else { FullImageBitmap := 0 FullImagePic.Value := PreviewCurrentPath } s := 18 ; scrollbar thickness FullImageHScroll := FullImageGui.Add("Slider", "x0 y0 w100 h" s " +ToolTip +AltSubmit") FullImageVScroll := FullImageGui.Add("Slider", "x0 y0 w" s " h100 +ToolTip +AltSubmit +Vertical") FullImageHScroll.OnEvent("Change", FullImageScrollChanged) FullImageVScroll.OnEvent("Change", FullImageScrollChanged) FullImageGui.OnEvent("Size", FullImageOnResize) FullImageGui.OnEvent("Close", CloseFullImageGui) ; Mouse interactions via message hooks (AHK v2 doesn't support Wheel/LButton events on Gui directly) OnMessage(0x020A, FullImage_WM_MOUSEWHEEL) ; WM_MOUSEWHEEL OnMessage(0x0200, FullImage_WM_MOUSEMOVE) ; WM_MOUSEMOVE OnMessage(0x0201, FullImage_WM_LBUTTONDOWN) ; WM_LBUTTONDOWN OnMessage(0x0202, FullImage_WM_LBUTTONUP) ; WM_LBUTTONUP ; Reset state FullImageScale := 1 FullImageIsDragging := false FullImageDragStartX := 0, FullImageDragStartY := 0 FullImageDragStartOffX := 0, FullImageDragStartOffY := 0 FullImageLastRenderedScale := 0 GetCurrentWorkArea(&waL, &waT, &waR, &waB) availW := Max(100, (waR - waL)) availH := Max(100, (waB - waT)) ; Calculate desired window size and clamp to available area desiredW := FullImageOrigW + s + 40 desiredH := FullImageOrigH + s + 60 if (desiredW > availW || desiredH > availH) { FullImageGui.Show("Maximize") ; Wait a moment for the window to finish maximizing Sleep(100) if (IsObject(FullImageGui)) { FullImageGui.GetClientPos(, , &clientW, &clientH) FullImageLayout(clientW, clientH) } return } initialW := Min(desiredW, availW) initialH := Min(desiredH, availH) ; Start from fixed origin (50, 50) relative to work area x := waL + 50 y := waT + 50 FullImageGui.Show("x" x " y" y " w" initialW " h" initialH) if (IsObject(FullImageGui)) { FullImageGui.GetClientPos(, , &clientW, &clientH) FullImageLayout(clientW, clientH) } } FullImageWebResize(gui, minMax, w, h) { ; legacy no-op } FullImageLayout(w, h) { global FullImagePic, FullImageOrigW, FullImageOrigH, FullImageHScroll, FullImageVScroll global FullImageScale, FullImageLastRenderedScale s := 18 viewportW := Max(1, w - s) viewportH := Max(1, h - s) ; Apply scaling scaledW := Max(1, Round(FullImageOrigW * FullImageScale)) scaledH := Max(1, Round(FullImageOrigH * FullImageScale)) maxX := Max(0, scaledW - viewportW) maxY := Max(0, scaledH - viewportH) FullImageHScroll.Opt("Range0-" maxX) FullImageVScroll.Opt("Range0-" maxY) FullImageHScroll.Move(0, viewportH, viewportW, s) FullImageVScroll.Move(viewportW, 0, s, viewportH) centerX := Max(0, Floor((viewportW - scaledW) / 2)) centerY := Max(0, Floor((viewportH - scaledH) / 2)) offX := Min(FullImageHScroll.Value, maxX) offY := Min(FullImageVScroll.Value, maxY) x := -offX + centerX y := -offY + centerY FullImagePic.Move(x, y, scaledW, scaledH) ; Re-assign bitmap at new size when scale changes (for quality with -Resize) if (FullImageLastRenderedScale != FullImageScale) { FullImageReRenderBitmap(scaledW, scaledH) FullImageLastRenderedScale := FullImageScale } } CloseFullImageGui(*) { global FullImageGui ReleaseFullImageBitmap() ; Unregister message hooks OnMessage(0x020A, FullImage_WM_MOUSEWHEEL, false) OnMessage(0x0200, FullImage_WM_MOUSEMOVE, false) OnMessage(0x0201, FullImage_WM_LBUTTONDOWN, false) OnMessage(0x0202, FullImage_WM_LBUTTONUP, false) FullImageGui := 0 } LoadFullImage() { ; Not used in slider-based viewer. } FullImageOnResize(gui, minMax, w, h) { global FullImageResizeTimer if (minMax = -1) return ; Debounce: delay layout recalculation until resize stops if (FullImageResizeTimer) SetTimer(FullImageResizeTimer, 0) ; cancel previous FullImageResizeTimer := () => DoFullImageResize(w, h) SetTimer(FullImageResizeTimer, -50) ; execute once after 50ms } DoFullImageResize(w, h) { global FullImageGui, FullImagePic if (!IsObject(FullImageGui)) return ; Suspend redraw during layout try SendMessage(0x000B, 0, 0, FullImagePic.Hwnd) ; WM_SETREDRAW off FullImageLayout(w, h) try SendMessage(0x000B, 1, 0, FullImagePic.Hwnd) ; WM_SETREDRAW on try DllCall("RedrawWindow", "Ptr", FullImageGui.Hwnd, "Ptr", 0, "Ptr", 0, "UInt", 0x0081) } FullImageScrollChanged(*) { global FullImageGui if (!IsObject(FullImageGui)) return FullImageGui.GetClientPos(, , &w, &h) FullImageLayout(w, h) } FullImageReRenderBitmap(w, h) { global PreviewOriginalPath, FullImageBitmap, FullImagePic ReleaseFullImageBitmap() ; Use high-quality scaling with LoadPicture from ORIGINAL full-size image ; For better performance, only re-render when scale changes significantly options := "W" w " H" h try { bmp := LoadPicture(PreviewOriginalPath, options) if (bmp) { FullImageBitmap := bmp FullImagePic.Value := "HBITMAP:" FullImageBitmap } else { FullImageBitmap := 0 FullImagePic.Value := PreviewOriginalPath } } catch { FullImageBitmap := 0 FullImagePic.Value := PreviewOriginalPath } } FullImageZoomAtCursor(factor) { global FullImageGui, FullImageHScroll, FullImageVScroll global FullImageOrigW, FullImageOrigH, FullImageScale ; Declare all local variables at function start local w, h, s, viewportW, viewportH local oldScale, newScale local mx, my, cx0, cy0, cx, cy local oldW, oldH, newW, newH local maxOldX, maxOldY, offX, offY local centerX, centerY, imgX, imgY local relX, relY local scaleRatio, newRelX, newRelY local newCenterX, newCenterY, newOffX, newOffY if (!IsObject(FullImageGui)) return FullImageGui.GetClientPos(, , &w, &h) s := 18 viewportW := Max(1, w - s) viewportH := Max(1, h - s) ; Clamp scale oldScale := FullImageScale newScale := oldScale * factor if (newScale < 0.1) newScale := 0.1 if (newScale > 8) newScale := 8 if (Abs(newScale - oldScale) < 0.0001) return ; Mouse position relative to client area (screen -> client) MouseGetPos(&mx, &my) WinGetClientPos(&cx0, &cy0, , , "ahk_id " FullImageGui.Hwnd) cx := mx - cx0 cy := my - cy0 oldW := Max(1, Round(FullImageOrigW * oldScale)) oldH := Max(1, Round(FullImageOrigH * oldScale)) newW := Max(1, Round(FullImageOrigW * newScale)) newH := Max(1, Round(FullImageOrigH * newScale)) ; Fallbacks if something is not yet ready if (viewportW <= 0 || viewportH <= 0 || oldW <= 0 || oldH <= 0) { FullImageScale := newScale FullImageLayout(w, h) return } maxOldX := Max(0, oldW - viewportW) maxOldY := Max(0, oldH - viewportH) offX := Min(FullImageHScroll.Value, maxOldX) offY := Min(FullImageVScroll.Value, maxOldY) centerX := Max(0, Floor((viewportW - oldW) / 2)) centerY := Max(0, Floor((viewportH - oldH) / 2)) imgX := -offX + centerX imgY := -offY + centerY ; Position of cursor relative to image (clamp within current image bounds) relX := cx - imgX relY := cy - imgY if (relX < 0) relX := 0 if (relY < 0) relY := 0 if (relX > oldW) relX := oldW if (relY > oldH) relY := oldH ; Scale change; preserve point under cursor if (oldScale = 0) scaleRatio := 1 else scaleRatio := newScale / oldScale newRelX := relX * scaleRatio newRelY := relY * scaleRatio newCenterX := Max(0, Floor((viewportW - newW) / 2)) newCenterY := Max(0, Floor((viewportH - newH) / 2)) newOffX := Clamp(newRelX - (cx - newCenterX), 0, Max(0, newW - viewportW)) newOffY := Clamp(newRelY - (cy - newCenterY), 0, Max(0, newH - viewportH)) FullImageScale := newScale FullImageHScroll.Value := newOffX FullImageVScroll.Value := newOffY FullImageLayout(w, h) } Clamp(val, minVal, maxVal) { if (val < minVal) return minVal if (val > maxVal) return maxVal return val } ; Message-based mouse handling scoped to FullImageGui and its child controls FullImage_WM_MOUSEWHEEL(wParam, lParam, msg, hwnd) { global FullImageGui, FullImagePic, FullImageHScroll, FullImageVScroll if (!IsObject(FullImageGui)) return if (hwnd != FullImageGui.Hwnd && hwnd != FullImagePic.Hwnd && hwnd != FullImageHScroll.Hwnd && hwnd != FullImageVScroll.Hwnd) return delta := (wParam >> 16) & 0xFFFF if (delta >= 0x8000) delta := delta - 0x10000 factor := (delta > 0) ? 1.1 : (1/1.1) FullImageZoomAtCursor(factor) } FullImage_WM_LBUTTONDOWN(wParam, lParam, msg, hwnd) { global FullImageGui, FullImagePic, FullImageHScroll, FullImageVScroll global FullImageIsDragging, FullImageDragStartX, FullImageDragStartY global FullImageDragStartOffX, FullImageDragStartOffY if (!IsObject(FullImageGui)) return if (hwnd != FullImageGui.Hwnd && hwnd != FullImagePic.Hwnd) return FullImageIsDragging := true MouseGetPos(&mx, &my) FullImageDragStartX := mx FullImageDragStartY := my FullImageDragStartOffX := FullImageHScroll.Value FullImageDragStartOffY := FullImageVScroll.Value } FullImage_WM_LBUTTONUP(wParam, lParam, msg, hwnd) { global FullImageGui, FullImageIsDragging if (!IsObject(FullImageGui)) return FullImageIsDragging := false } FullImage_WM_MOUSEMOVE(wParam, lParam, msg, hwnd) { global FullImageGui, FullImagePic, FullImageIsDragging global FullImageDragStartX, FullImageDragStartY global FullImageDragStartOffX, FullImageDragStartOffY global FullImageHScroll, FullImageVScroll global FullImageOrigW, FullImageOrigH, FullImageScale if (!IsObject(FullImageGui)) return if (!FullImageIsDragging) return ; Only react when moving over the full image window or picture if (hwnd != FullImageGui.Hwnd && hwnd != FullImagePic.Hwnd) return MouseGetPos(&mx, &my) dx := mx - FullImageDragStartX dy := my - FullImageDragStartY ; Compute current maxima based on scale and viewport FullImageGui.GetClientPos(, , &w, &h) s := 18 viewportW := Max(1, w - s) viewportH := Max(1, h - s) scaledW := Max(1, Round(FullImageOrigW * FullImageScale)) scaledH := Max(1, Round(FullImageOrigH * FullImageScale)) maxX := Max(0, scaledW - viewportW) maxY := Max(0, scaledH - viewportH) newOffX := Clamp(FullImageDragStartOffX - dx, 0, maxX) newOffY := Clamp(FullImageDragStartOffY - dy, 0, maxY) FullImageHScroll.Value := newOffX FullImageVScroll.Value := newOffY FullImageLayout(w, h) } ; -------------------- CRUD: New / Edit / Delete -------------------- NewDocument(*) { OpenDocumentForm() } EditDocument(*) { global CurrentSelectedId if (!CurrentSelectedId) { MsgBox "Veuillez selectionner un document a modifier.", "Attention", 48 return } idx := FindDocIndexById(CurrentSelectedId) if (!idx) { MsgBox "Document introuvable.", "Erreur", 16 return } OpenDocumentForm(Documents[idx]) } AddModel(*) { global CurrentSelectedId, Documents if (!CurrentSelectedId) { MsgBox "Veuillez selectionner un document pour ajouter un modele.", "Attention", 48 return } idx := FindDocIndexById(CurrentSelectedId) if (!idx) { MsgBox "Document introuvable.", "Erreur", 16 return } OpenDocumentForm(0, Documents[idx]) ; 0 = new document, baseDoc = template } DeleteDocument(*) { global CurrentSelectedId, Documents if (!CurrentSelectedId) { MsgBox "Veuillez selectionner un document a supprimer.", "Attention", 48 return } ; Find the document to get its title idx := FindDocIndexById(CurrentSelectedId) if (!idx) return doc := Documents[idx] title := doc.Titre modelCount := GetDocumentsByTitle(title).Length if (modelCount > 1) { if (MsgBox("Ce document a " . modelCount . " modeles. Voulez-vous supprimer TOUS les modeles de '" . title . "'?", "Confirmation", 0x4) != "Yes") return ; Delete all models with the same title indicesToDelete := [] for i, doc in Documents { if (doc.Titre = title) { indicesToDelete.Push(i) } } ; Delete in reverse order to maintain indices ; Sort indices in reverse order (manual sort) i := 1 while i <= indicesToDelete.Length { j := i + 1 while j <= indicesToDelete.Length { if (indicesToDelete[i] < indicesToDelete[j]) { temp := indicesToDelete[i] indicesToDelete[i] := indicesToDelete[j] indicesToDelete[j] := temp } j++ } i++ } for idx in indicesToDelete { Documents.RemoveAt(idx) } } else { if (MsgBox("Voulez-vous vraiment supprimer ce document?", "Confirmation", 0x4) != "Yes") return Documents.RemoveAt(idx) } SaveData() ApplyFilters() } FindDocIndexById(id) { global Documents for i, doc in Documents { if (doc.Id = id) return i } return 0 } GenerateNextId() { global CounterFile, Documents maxId := 0 ; Inspect existing documents for doc in Documents { try { val := Integer(doc.Id) if (val > maxId) maxId := val } catch { continue } } ; Read persisted counter currentCounter := 0 if FileExist(CounterFile) { try { currentCounter := Integer(Trim(FileRead(CounterFile))) } catch { currentCounter := 0 } } ; Choose the larger source if (currentCounter > maxId) maxId := currentCounter nextId := maxId + 1 ; Persist try { FileOpen(CounterFile, "w", "UTF-8").Write(String(nextId)) } catch Error as e { MsgBox("Erreur lors de la sauvegarde du compteur: " . e.Message, "Erreur", 16) } return String(nextId) } GetModelNumber(doc) { global Documents title := doc.Titre sameTitleDocs := [] for d in Documents { if (d.Titre = title) { sameTitleDocs.Push(d) } } ; Sort by ID to get consistent model numbers (manual sort) i := 1 while i <= sameTitleDocs.Length { j := i + 1 while j <= sameTitleDocs.Length { if (Integer(sameTitleDocs[i].Id) > Integer(sameTitleDocs[j].Id)) { temp := sameTitleDocs[i] sameTitleDocs[i] := sameTitleDocs[j] sameTitleDocs[j] := temp } j++ } i++ } for i, d in sameTitleDocs { if (d.Id = doc.Id) { return String(i) } } return "1" } GetDocumentsByTitle(title) { global Documents result := [] for doc in Documents { if (doc.Titre = title) { result.Push(doc) } } return result } UpdateAllModels(title, updates, excludeId := "") { global Documents for i, doc in Documents { if (doc.Titre = title && doc.Id != excludeId) { ; Update shared fields (excluding the document being edited) if (updates.Has("Tag")) doc.Tag := updates["Tag"] if (updates.Has("ClasseLot")) doc.ClasseLot := updates["ClasseLot"] if (updates.Has("Gouvernement")) doc.Gouvernement := updates["Gouvernement"] if (updates.Has("DocumentKofax")) doc.DocumentKofax := updates["DocumentKofax"] if (updates.Has("Filename")) doc.Filename := updates["Filename"] } } } OpenDocumentForm(existingDoc := 0, baseDoc := 0) { global Gouvernements, ClassesLot, DocumentsKofax global KofaxPerClass, MainGui if (!IsSet(KofaxPerClass)) KofaxPerClass := ReadKofaxPerClass() title := existingDoc ? "Modifier Document" : (baseDoc ? "Nouveau Modele" : "Nouveau Document") g := Gui("+Resize +MinSize650x500 +Owner" MainGui.Hwnd, title) g.MarginX := 15, g.MarginY := 15 MainGui.Opt("+Disabled") g.OnEvent("Close", (*) => (MainGui.Opt("-Disabled"), g.Destroy())) ; Title section g.Add("Text", "x15 y15 w120 h20", "Titre du document:") eTitre := g.Add("Edit", "x150 y13 w400 h22") ; Tag section g.Add("Text", "x15 y50 w120 h20", "Tag:") eTag := g.Add("Edit", "x150 y48 w120 h22") ; Dropdowns section g.Add("Text", "x15 y85 w120 h20", "Classe de lot:") ddlClasse := g.Add("DropDownList", "x150 y83 w250 h200", ClassesLot.Length ? ClassesLot : []) g.Add("Text", "x420 y85 w100 h20", "Gouvernement:") ddlGov := g.Add("DropDownList", "x530 y83 w250 h200", Gouvernements.Length ? Gouvernements : []) g.Add("Text", "x15 y120 w120 h20", "Document Kofax:") ddlKofax := g.Add("DropDownList", "x150 y118 w250 h200", []) updateKofax := (*) => ( cls := ddlClasse.Text, items := GetKofaxForClass(KofaxPerClass, cls), ddlKofax.Delete(), items.Length ? ddlKofax.Add(items) : 0 ) ddlClasse.OnEvent("Change", updateKofax) ; Initialize with first class for new documents if (!existingDoc && !baseDoc && ClassesLot.Length > 0) { ddlClasse.Choose(1) updateKofax() } ; File section g.Add("Text", "x15 y155 w120 h20", "Fichier image:") eFilename := g.Add("Edit", "x150 y153 w350 h22 ReadOnly") btnFile := g.Add("Button", "x510 y152 w80 h24", "Parcourir") btnPaste := g.Add("Button", "x600 y152 w120 h24", "Coller Image") pPreview := g.Add("Picture", "x15 y185 w200 h150 Border") ; If creating a model, clear image controls (models shouldn't carry images) if (baseDoc && !existingDoc) { eFilename.Value := "" pPreview.Value := "" } ; Directives section g.Add("Text", "x15 y350 w120 h20", "Directives de validation:") eDirectives := g.Add("Edit", "x15 y375 w620 h80 +Multi +VScroll") ; Keywords section g.Add("Text", "x15 y470 w120 h20", "Mots-clés:") eKeywords := g.Add("Edit", "x15 y495 w620 h80 +Multi +VScroll") ; Buttons section btnSave := g.Add("Button", "x500 y590 w100 h30", existingDoc ? "Sauvegarder" : "Creer") btnCancel := g.Add("Button", "x610 y590 w80 h30", "Annuler") ; Populate fields srcDoc := existingDoc ? existingDoc : baseDoc if (srcDoc) { eTitre.Value := srcDoc.Titre eTag.Value := srcDoc.Tag ; Set Classe de lot first classeFound := false if (srcDoc.ClasseLot) { for i, item in ClassesLot { if (item = srcDoc.ClasseLot) { ddlClasse.Choose(i) classeFound := true break } } } ; Set Gouvernement if (srcDoc.Gouvernement) { for i, item in Gouvernements { if (item = srcDoc.Gouvernement) { ddlGov.Choose(i) break } } } ; Update Kofax list based on selected class AFTER class is selected if (classeFound) { updateKofax() } ; Then set Document Kofax if (srcDoc.DocumentKofax && classeFound) { kofaxItems := GetKofaxForClass(KofaxPerClass, ddlClasse.Text) for i, item in kofaxItems { if (item = srcDoc.DocumentKofax) { ddlKofax.Choose(i) break } } } if (srcDoc.Filename && FileExist(ImagesDir "\" srcDoc.Filename)) { eFilename.Value := srcDoc.Filename pPreview.Value := ImagesDir "\" srcDoc.Filename } if (srcDoc.Directives) eDirectives.Value := srcDoc.Directives if (srcDoc.Keywords) eKeywords.Value := srcDoc.Keywords ; If creating model from baseDoc, clear the image if (baseDoc && !existingDoc) { eFilename.Value := "" pPreview.Value := "" } } btnFile.OnEvent("Click", (*) => ChooseFileForEdit(eFilename, pPreview)) btnPaste.OnEvent("Click", (*) => PasteImageFromClipboard(eFilename, pPreview)) btnSave.OnEvent("Click", (*) => SaveDocFromForm(g, existingDoc, eTitre, eTag, ddlClasse, ddlGov, ddlKofax, eFilename, eDirectives, eKeywords)) btnCancel.OnEvent("Click", (*) => (MainGui.Opt("-Disabled"), g.Destroy())) g.Show("w850 h640") } ChooseFileForEdit(eFilename, pPreview) { try { f := FileSelect(3, , "Choisir une image") if (!f) return eFilename.Value := CopyImageToLibrary(f) full := ImagesDir "\" eFilename.Value if FileExist(full) pPreview.Value := full } catch as e { MsgBox("Erreur lors de l'importation de l'image:`n" e.Message "`n`nStack:`n" e.Stack, "Erreur", 16) } } CopyImageToLibrary(src) { global ImagesDir, ThumbnailsDir SplitPath src, &name, &dir, &ext, &nameNoExt if !DirExist(ImagesDir) DirCreate(ImagesDir) if !DirExist(ThumbnailsDir) DirCreate(ThumbnailsDir) target := name i := 1 while FileExist(ImagesDir "\" target) { suffix := Format("_{:03}", i) target := nameNoExt suffix "." ext i++ } full := ImagesDir "\" target FileCopy src, full, true ; Generate preview 500px max (width or height), preserving aspect GeneratePreview(full) return target } GeneratePreview(srcFullPath) { global ThumbnailsDir ; Ensure previews directory exists if !DirExist(ThumbnailsDir) DirCreate(ThumbnailsDir) ; Compute destination preview path as JPG with same base name SplitPath srcFullPath, &srcName, &srcDir, &srcExt, &srcBase destFullPath := ThumbnailsDir "\" srcBase ".jpg" ; Load original dimensions dims := GetImageDimensions(srcFullPath) w := dims["w"], h := dims["h"] if (w <= 0 || h <= 0) { try FileCopy(srcFullPath, destFullPath, true) return } maxDim := 1000 ; If already smaller than max, just copy if (w <= maxDim && h <= maxDim) { try FileCopy(srcFullPath, destFullPath, true) return } ; Calculate scaled dimensions - longest side = 500px if (w > h) { outW := maxDim outH := Max(1, Round(h * maxDim / w)) } else { outH := maxDim outW := Max(1, Round(w * maxDim / h)) } ; Use GDI+ directly to load and save scaled result := SaveScaledImageGdip(srcFullPath, destFullPath, outW, outH) if (result) { return } ; Fallback: copy original (this shouldn't happen) try FileCopy(srcFullPath, destFullPath, true) } SaveScaledImageGdip(srcPath, destPath, targetW, targetH) { ; Initialize GDI+ once static gdipStarted := false, gdipToken := 0 if (!gdipStarted) { if !DllCall("LoadLibrary", "Str", "gdiplus", "Ptr") return false si := Buffer(24, 0) NumPut("UInt", 1, si, 0) if (DllCall("gdiplus\GdiplusStartup", "Ptr*", &gdipToken, "Ptr", si, "Ptr", 0) != 0) return false gdipStarted := true } ; Load source image with GDI+ (wide string) pBitmapSrc := 0 if (DllCall("gdiplus\GdipLoadImageFromFile", "WStr", srcPath, "Ptr*", &pBitmapSrc) != 0 || !pBitmapSrc) return false ; Create destination bitmap with target dimensions (32bppARGB) pBitmapDest := 0 if (DllCall("gdiplus\GdipCreateBitmapFromScan0", "Int", targetW, "Int", targetH, "Int", 0, "Int", 0x26200A, "Ptr", 0, "Ptr*", &pBitmapDest) != 0 || !pBitmapDest) { DllCall("gdiplus\GdipDisposeImage", "Ptr", pBitmapSrc) return false } ; Get graphics context for destination pGraphics := 0 if (DllCall("gdiplus\GdipGetImageGraphicsContext", "Ptr", pBitmapDest, "Ptr*", &pGraphics) != 0 || !pGraphics) { DllCall("gdiplus\GdipDisposeImage", "Ptr", pBitmapSrc) DllCall("gdiplus\GdipDisposeImage", "Ptr", pBitmapDest) return false } ; Set high quality interpolation mode DllCall("gdiplus\GdipSetInterpolationMode", "Ptr", pGraphics, "Int", 7) ; HighQualityBicubic DllCall("gdiplus\GdipSetSmoothingMode", "Ptr", pGraphics, "Int", 4) ; AntiAlias DllCall("gdiplus\GdipSetPixelOffsetMode", "Ptr", pGraphics, "Int", 4) ; HighQuality ; Draw source onto destination scaled DllCall("gdiplus\GdipDrawImageRectI", "Ptr", pGraphics, "Ptr", pBitmapSrc, "Int", 0, "Int", 0, "Int", targetW, "Int", targetH) ; Get JPEG encoder CLSID (hardcoded - universal across Windows) ; {557CF401-1A04-11D3-9A73-0000F81EF32E} encClsid := Buffer(16, 0) NumPut("UInt", 0x557CF401, encClsid, 0) NumPut("UShort", 0x1A04, encClsid, 4) NumPut("UShort", 0x11D3, encClsid, 6) NumPut("UChar", 0x9A, encClsid, 8) NumPut("UChar", 0x73, encClsid, 9) NumPut("UChar", 0x00, encClsid, 10) NumPut("UChar", 0x00, encClsid, 11) NumPut("UChar", 0xF8, encClsid, 12) NumPut("UChar", 0x1E, encClsid, 13) NumPut("UChar", 0xF3, encClsid, 14) NumPut("UChar", 0x2E, encClsid, 15) ; Save destination bitmap to JPEG with default quality (~75) ok := (DllCall("gdiplus\GdipSaveImageToFile", "Ptr", pBitmapDest, "WStr", destPath, "Ptr", encClsid, "Ptr", 0) = 0) ; Cleanup DllCall("gdiplus\GdipDeleteGraphics", "Ptr", pGraphics) DllCall("gdiplus\GdipDisposeImage", "Ptr", pBitmapSrc) DllCall("gdiplus\GdipDisposeImage", "Ptr", pBitmapDest) return ok } Gdip_GetEncoderClsid(mimeType, clsidBuf) { num := 0, size := 0 status := DllCall("gdiplus\GdipGetImageEncodersSize", "UInt*", &num, "UInt*", &size) if (status != 0 || size = 0) { MsgBox("GdipGetImageEncodersSize failed: " status " num=" num " size=" size, "Encoder Debug", 48) return false } bi := Buffer(size, 0) status := DllCall("gdiplus\GdipGetImageEncoders", "UInt", num, "UInt", size, "Ptr", bi) if (status != 0) { MsgBox("GdipGetImageEncoders failed: " status, "Encoder Debug", 48) return false } p := bi.Ptr structSize := (A_PtrSize = 8 ? 88 : 76) ; ImageCodecInfo size differs by architecture loop num { ; ImageCodecInfo structure: CLSID is at offset 0, MimeType pointer is at offset 48 pMime := NumGet(p, 48, "Ptr") if (pMime) { try { mime := StrGet(pMime, "UTF-16") if (mime = mimeType) { DllCall("RtlMoveMemory", "Ptr", clsidBuf.Ptr, "Ptr", p, "UPtr", 16) return true } } catch { ; Skip invalid mime pointer } } p += structSize } MsgBox("PNG encoder not found among " num " encoders", "Encoder Debug", 48) return false } SaveDocFromForm(g, existingDoc, eTitre, eTag, ddlClasse, ddlGov, ddlKofax, eFilename, eDirectives, eKeywords) { global Documents ; Validate required fields: Title, Tag, Classe, Gouvernement, DocumentKofax, Filename (when not model) missing := [] if (!Trim(eTitre.Value)) missing.Push("Titre") if (!Trim(eTag.Value)) missing.Push("Tag") if (!ddlClasse.Text) missing.Push("Classe de lot") if (!ddlGov.Text) missing.Push("Gouvernement") if (!ddlKofax.Text) missing.Push("Document Kofax") ; Filename required unless creating a model (we infer model by empty filename AND existingDoc is false) if (!Trim(eFilename.Value) && !existingDoc) missing.Push("Fichier image") if (missing.Length) { MsgBox("Champs requis manquants: `n- " . JoinText(missing, "`n- "), "Champs incomplets", 48) return } doc := Map() doc.Id := existingDoc ? existingDoc.Id : GenerateNextId() doc.Titre := Trim(eTitre.Value) doc.Tag := Trim(eTag.Value) doc.ClasseLot := ddlClasse.Text doc.Gouvernement := ddlGov.Text doc.DocumentKofax := ddlKofax.Text doc.Directives := eDirectives.Value doc.Keywords := eKeywords.Value doc.Filename := eFilename.Value if (existingDoc) { idx := FindDocIndexById(existingDoc.Id) if (idx) { Documents[idx] := doc ; Update shared fields for all models of the same title (but NOT Filename - each model has its own image) updates := Map() updates["Tag"] := doc.Tag updates["ClasseLot"] := doc.ClasseLot updates["Gouvernement"] := doc.Gouvernement updates["DocumentKofax"] := doc.DocumentKofax UpdateAllModels(doc.Titre, updates, existingDoc.Id) } } else { Documents.Push(doc) } SaveData() ApplyFilters() MainGui.Opt("-Disabled") g.Destroy() } PasteImageFromClipboard(eFilename, pPreview) { global ImagesDir, ThumbnailsDir try { if !DirExist(ImagesDir) DirCreate(ImagesDir) ts := FormatTime(A_Now, "yyyyMMdd-HHmmss") ; Case 1: Clipboard contains a file path to an image clipText := A_Clipboard if (Type(clipText) = "String" && FileExist(clipText)) { SplitPath clipText, , , &ext if (InStr(",jpg,jpeg,png,bmp,gif,jfif,webp,svg,", "," . StrLower(ext) . ",")) { name := "clip_" ts "." ext FileCopy clipText, ImagesDir "\" name, true if !DirExist(ThumbnailsDir) DirCreate(ThumbnailsDir) GeneratePreview(ImagesDir "\" name) eFilename.Value := name full := ImagesDir "\" name if FileExist(full) pPreview.Value := full return } } ; Case 2: Clipboard contains a DIB image (CF_DIB) CF_DIB := 8 if DllCall("IsClipboardFormatAvailable", "UInt", CF_DIB) { if DllCall("OpenClipboard", "Ptr", 0) { hData := DllCall("GetClipboardData", "UInt", CF_DIB, "Ptr") if (hData) { pDIB := DllCall("GlobalLock", "Ptr", hData, "Ptr") if (pDIB) { size := DllCall("GlobalSize", "Ptr", hData, "UPtr") ; Build BMP file buffer: add 14-byte BITMAPFILEHEADER before DIB fileSize := size + 14 buf := Buffer(fileSize, 0) ; BITMAPFILEHEADER NumPut("UShort", 0x4D42, buf, 0) ; 'BM' NumPut("UInt", fileSize, buf, 2) NumPut("UShort", 0, buf, 6) NumPut("UShort", 0, buf, 8) ; Compute bfOffBits = 14 + biSize + paletteBytes biSize := NumGet(pDIB, 0, "UInt") biBitCount := NumGet(pDIB, 14, "UShort") biCompression := NumGet(pDIB, 16, "UInt") biClrUsed := NumGet(pDIB, 32, "UInt") paletteColors := (biBitCount <= 8) ? (biClrUsed ? biClrUsed : (1 << biBitCount)) : 0 paletteBytes := paletteColors * 4 if ((biBitCount = 16 || biBitCount = 32) && biCompression = 3) paletteBytes += 12 ; BI_BITFIELDS masks bfOffBits := 14 + biSize + paletteBytes NumPut("UInt", bfOffBits, buf, 10) ; Copy DIB after file header DllCall("RtlMoveMemory", "Ptr", buf.Ptr + 14, "Ptr", pDIB, "UPtr", size) DllCall("GlobalUnlock", "Ptr", hData) DllCall("CloseClipboard") ; Write file name := "clip_" ts ".bmp" full := ImagesDir "\" name FileOpen(full, "w").RawWrite(buf) eFilename.Value := name if !DirExist(ThumbnailsDir) DirCreate(ThumbnailsDir) GeneratePreview(full) if FileExist(full) pPreview.Value := full return } DllCall("GlobalUnlock", "Ptr", hData) } DllCall("CloseClipboard") } } MsgBox "Aucune image valide dans le presse-papiers.", "Information", 64 } } ; -------------------- Save / Load -------------------- SaveData() { global DataFile, Documents ; Create backup before saving if FileExist(DataFile) { try FileCopy(DataFile, DataFile ".backup", true) } ; Simple custom format - human readable, preserves line breaks output := "" for doc in Documents { output .= "##allobob##DOCUMENT##allobob##`n" output .= "##allobob##Id##allobob##`n" . doc.Id . "`n" output .= "##allobob##Titre##allobob##`n" . doc.Titre . "`n" output .= "##allobob##Tag##allobob##`n" . doc.Tag . "`n" output .= "##allobob##ClasseLot##allobob##`n" . doc.ClasseLot . "`n" output .= "##allobob##DocumentKofax##allobob##`n" . doc.DocumentKofax . "`n" output .= "##allobob##Gouvernement##allobob##`n" . doc.Gouvernement . "`n" output .= "##allobob##Directives##allobob##`n" . doc.Directives . "`n" output .= "##allobob##Keywords##allobob##`n" . doc.Keywords . "`n" output .= "##allobob##Filename##allobob##`n" . doc.Filename . "`n" output .= "##allobob##END##allobob##`n`n" } try { FileOpen(DataFile, "w", "UTF-8").Write(output) } catch Error as e { MsgBox("Erreur lors de la sauvegarde: " . e.Message, "Erreur", 16) } } JsonStringify(obj, indent := "") { if (Type(obj) = "Array") { if (obj.Length = 0) return "[]" result := "[`n" for i, item in obj { result .= indent " " JsonStringify(item, indent " ") if (i < obj.Length) result .= "," result .= "`n" } result .= indent "]" return result } else if (Type(obj) = "Map") { items := [] for key, value in obj { items.Push(key) } if (items.Length = 0) return "{}" result := "{`n" for i, key in items { result .= indent " " JsonEscape(key) ": " JsonStringify(obj[key], indent " ") if (i < items.Length) result .= "," result .= "`n" } result .= indent "}" return result } else if (Type(obj) = "Object") { ; Handle standard objects using ObjOwnProps items := [] for key in obj.OwnProps() { items.Push(key) } if (items.Length = 0) return "{}" result := "{`n" for i, key in items { result .= indent " " JsonEscape(key) ": " JsonStringify(obj.%key%, indent " ") if (i < items.Length) result .= "," result .= "`n" } result .= indent "}" return result } else if (Type(obj) = "String") { return JsonEscape(obj) } else if (Type(obj) = "Integer" || Type(obj) = "Float") { return String(obj) } else if (obj = "" || !IsSet(obj)) { return '""' } return '""' } JsonEscape(str) { if (str = "" || !IsSet(str)) return '""' str := StrReplace(str, "\", "\\") str := StrReplace(str, '"', '\"') str := StrReplace(str, "`n", "\n") str := StrReplace(str, "`r", "\r") str := StrReplace(str, "`t", "\t") return '"' str '"' } JoinText(arr, sep := ", ") { out := "" for i, v in arr out .= (i > 1 ? sep : "") . v return out } ; -------------------- Manage Lists -------------------- ManageLists(*) { global Gouvernements, ClassesLot, DocumentsKofax global KofaxPerClass, MainGui if (!IsSet(KofaxPerClass)) KofaxPerClass := ReadKofaxPerClass() mg := Gui("+Resize +MinSize800x500 +Owner" MainGui.Hwnd, "Gerer les listes de choix") mg.MarginX := 15, mg.MarginY := 15 MainGui.Opt("+Disabled") ; Gouvernement section mg.Add("Text", "x15 y15 w150 h20", "Gouvernements:") lbGov := mg.Add("ListBox", "x15 y40 w250 h200", Gouvernements) ; Classe Lot section mg.Add("Text", "x15 y260 w150 h20", "Classes de lot:") lbClasse := mg.Add("ListBox", "x15 y285 w250 h200", ClassesLot) ; Document Kofax section (per-class) mg.Add("Text", "x400 y15 w150 h20", "Documents Kofax (par classe):") mg.Add("Text", "x400 y40 w90 h20", "Classe:") ddlKofaxClass := mg.Add("DropDownList", "x460 y38 w190 h200", ClassesLot.Length ? ClassesLot : []) lbKofax := mg.Add("ListBox", "x400 y70 w250 h350", []) autoSave := (*) => SaveListsAndApply(lbGov, lbClasse, lbKofax, ddlKofaxClass) ; Buttons for Gouvernement mg.Add("Button", "x280 y40 w80 h25", "Ajouter").OnEvent("Click", (*) => (ListAdd(lbGov), autoSave())) mg.Add("Button", "x280 y70 w80 h25", "Modifier").OnEvent("Click", (*) => (ListEdit(lbGov, "gov"), autoSave())) mg.Add("Button", "x280 y100 w80 h25", "Supprimer").OnEvent("Click", (*) => (ListDelete(lbGov), autoSave())) ; Buttons for Classe Lot mg.Add("Button", "x280 y285 w80 h25", "Ajouter").OnEvent("Click", (*) => (ListAdd(lbClasse, "classe"), autoSave())) mg.Add("Button", "x280 y315 w80 h25", "Modifier").OnEvent("Click", (*) => (ListEdit(lbClasse, "classe"), autoSave())) mg.Add("Button", "x280 y345 w80 h25", "Supprimer").OnEvent("Click", (*) => (ListDelete(lbClasse, "classe"), autoSave())) ; Kofax per-class load when switching classes loadKofaxForClass := (*) => ( lbKofax.Delete(), cls := ddlKofaxClass.Text, items := GetKofaxForClass(KofaxPerClass, cls), items.Length ? lbKofax.Add(items) : 0 ) if (ClassesLot.Length) ddlKofaxClass.Choose(1) ddlKofaxClass.OnEvent("Change", loadKofaxForClass) loadKofaxForClass() ; Buttons for Kofax list mg.Add("Button", "x665 y70 w80 h25", "Ajouter").OnEvent("Click", (*) => (ListAdd(lbKofax), autoSave())) mg.Add("Button", "x665 y100 w80 h25", "Modifier").OnEvent("Click", (*) => (ListEdit(lbKofax, "kofax"), autoSave())) mg.Add("Button", "x665 y130 w80 h25", "Supprimer").OnEvent("Click", (*) => (ListDelete(lbKofax), autoSave())) ; Owner option makes it modal mg.OnEvent("Close", (*) => (MainGui.Opt("-Disabled"), mg.Destroy())) mg.Add("Button", "x540 y500 w100 h35", "Fermer").OnEvent("Click", (*) => (MainGui.Opt("-Disabled"), mg.Destroy())) mg.Show("w800 h550") } ListAdd(lb, kind := "") { global KofaxPerClass val := InputBox("Entrez une valeur", "Ajouter").Value if (val) { lb.Add([val]) if (kind = "classe") { if !IsSet(KofaxPerClass) KofaxPerClass := ReadKofaxPerClass() if !KofaxPerClass.Has(val) KofaxPerClass[val] := [] } } } ListDelete(lb, kind := "") { global KofaxPerClass idx := lb.Value if (!idx) return old := lb.Text lb.Delete(idx) if (kind = "classe" && IsSet(KofaxPerClass) && KofaxPerClass.Has(old)) KofaxPerClass.Delete(old) } ListEdit(lb, kind) { global KofaxPerClass idx := lb.Value if (!idx) return old := lb.Text result := InputBox("Nouveau nom", "Editer", , old) if (result.Result != "OK") return new := result.Value if (!new || new = old) return ; Insert new and remove old at same position for ListBox zeroIdx := idx - 1 DllCall("SendMessageW", "Ptr", lb.Hwnd, "UInt", 0x0181, "Ptr", zeroIdx, "Str", new) ; LB_INSERTSTRING lb.Delete(idx + 1) lb.Choose(idx) ; Propagate rename to documents PropagateRename(kind, old, new) ; Also rename Kofax map key if class renamed if (kind = "classe" && IsSet(KofaxPerClass) && KofaxPerClass.Has(old)) { KofaxPerClass[new] := KofaxPerClass[old] KofaxPerClass.Delete(old) } SaveData() ApplyFilters() } PropagateRename(kind, old, new) { global Documents for i, doc in Documents { if (kind = "gov" && doc.Gouvernement = old) doc.Gouvernement := new else if (kind = "classe" && doc.ClasseLot = old) doc.ClasseLot := new else if (kind = "kofax" && doc.DocumentKofax = old) doc.DocumentKofax := new } } SaveListsAndApply(lbGov, lbClasse, lbKofax, ddlKofaxClass?) { global Gouvernements, ClassesLot, DocumentsKofax, ListsDir, KofaxPerClass Gouvernements := lbItems(lbGov) ClassesLot := lbItems(lbClasse) ; Update KofaxPerClass only for current selected class if (IsSet(ddlKofaxClass)) { currentClass := ddlKofaxClass.Text KofaxPerClass[currentClass] := lbItems(lbKofax) WriteKofaxPerClass(KofaxPerClass) } ; Keep legacy global list for backward compatibility (first class list or existing global) DocumentsKofax := IsSet(ddlKofaxClass) ? GetKofaxForClass(KofaxPerClass, ddlKofaxClass.Text) : lbItems(lbKofax) WriteList(ListsDir "\\gouvernements.txt", Gouvernements) WriteList(ListsDir "\\classes_lot.txt", ClassesLot) ; Do not write DocumentsKofax to legacy file directly; managed by WriteKofaxPerClass } lbItems(lb) { arr := [] ; LB_GETCOUNT = 0x018B count := DllCall("SendMessageW", "Ptr", lb.Hwnd, "UInt", 0x018B, "Ptr", 0, "Ptr", 0, "Int") Loop count { idx := A_Index - 1 ; LB_GETTEXTLEN = 0x018A len := DllCall("SendMessageW", "Ptr", lb.Hwnd, "UInt", 0x018A, "Ptr", idx, "Ptr", 0, "Int") buf := Buffer((len + 1) * 2, 0) ; LB_GETTEXT = 0x0189 DllCall("SendMessageW", "Ptr", lb.Hwnd, "UInt", 0x0189, "Ptr", idx, "Ptr", buf.Ptr, "Int") arr.Push(StrGet(buf, "UTF-16")) } return arr } WriteList(path, arr) { txt := "" for v in arr txt .= v "`n" try { FileOpen(path, "w", "UTF-8").Write(txt) } catch Error as e { MsgBox("Erreur lors de l'ecriture de " . path . ": " . e.Message, "Erreur", 16) } } LoadLists() { global ListsDir, Gouvernements, ClassesLot, DocumentsKofax Gouvernements := ReadList(ListsDir "\gouvernements.txt") ClassesLot := ReadList(ListsDir "\classes_lot.txt") DocumentsKofax := ReadList(ListsDir "\documents_kofax.txt") } ReadList(path) { list := [] if FileExist(path) { for line in StrSplit(FileRead(path, "UTF-8"), ["`r`n","`n","`r"]) { line := Trim(line) if (line) list.Push(line) } } return list } ReadKofaxPerClass() { global ListsDir kmap := Map() path := ListsDir "\documents_kofax.txt" if !FileExist(path) { return kmap } txt := FileRead(path, "UTF-8") current := "" for line in StrSplit(txt, ["`r`n","`n","`r"]) { line := Trim(line) if (line = "") continue ; Trim BOM if present at start of file/line if (SubStr(line, 1, 1) = Chr(0xFEFF)) line := SubStr(line, 2) if (SubStr(line, 1, 1) = "[") { ; Section header [Class Name] if (SubStr(line, -1) = "]") current := SubStr(line, 2, StrLen(line) - 2) else current := Trim(RegExReplace(line, "^\[|\]$")) if !kmap.Has(current) kmap[current] := [] continue } if (current = "") { ; No section header yet: treat as global default list current := "__GLOBAL__" if !kmap.Has(current) kmap[current] := [] } kmap[current].Push(line) } return kmap } WriteKofaxPerClass(map) { global ListsDir path := ListsDir "\documents_kofax.txt" out := "" ; Write global first if exists if (map.Has("__GLOBAL__")) { for v in map["__GLOBAL__"] out .= v "`n" out .= "`n" } for k, arr in map { if (k = "__GLOBAL__") continue out .= "[" k "]`n" for v in arr out .= v "`n" out .= "`n" } FileOpen(path, "w", "UTF-8").Write(out) } GetKofaxForClass(kofaxMap, classe) { if (kofaxMap.Has(classe) && kofaxMap[classe].Length) return kofaxMap[classe] if (kofaxMap.Has("__GLOBAL__")) return kofaxMap["__GLOBAL__"] return [] } LoadData() { global DataFile, Documents Documents := [] if !FileExist(DataFile) return content := FileRead(DataFile, "UTF-8") ; Detect format if (InStr(content, "##allobob##DOCUMENT##allobob##")) { ; New simple format LoadDataSimple(content) } else if (InStr(content, "##DOCUMENT##")) { ; Old simple format v1 - migrate LoadDataSimpleV1(content) SaveData() } else if (InStr(content, "[DOCUMENT]")) { ; Old simple format v0 - migrate LoadDataSimpleOld(content) SaveData() } else if (SubStr(Trim(content), 1, 1) = "[" || SubStr(Trim(content), 1, 1) = "{") { ; Old JSON format - migrate MsgBox("Ancien format JSON détecté. Migration vers le nouveau format...", "Migration", 64) try { parsed := Jxon_Load(&content) for doc in parsed { docMap := Map() docMap.Id := doc.Has("Id") ? doc["Id"] : "" docMap.Titre := doc.Has("Titre") ? doc["Titre"] : "" docMap.Tag := doc.Has("Tag") ? doc["Tag"] : "" docMap.ClasseLot := doc.Has("ClasseLot") ? doc["ClasseLot"] : "" docMap.DocumentKofax := doc.Has("DocumentKofax") ? doc["DocumentKofax"] : "" docMap.Gouvernement := doc.Has("Gouvernement") ? doc["Gouvernement"] : "" docMap.Directives := doc.Has("Directives") ? doc["Directives"] : "" docMap.Keywords := doc.Has("Keywords") ? doc["Keywords"] : "" docMap.Filename := doc.Has("Filename") ? doc["Filename"] : "" Documents.Push(docMap) } SaveData() MsgBox("Migration réussie!", "Succès", 64) } catch { MsgBox("Erreur de migration. Démarrage avec base vide.", "Erreur", 16) } } else { ; Very old pipe format MsgBox("Ancien format détecté. Migration en cours...", "Migration", 64) LoadDataLegacy(content) SaveData() MsgBox("Migration terminée!", "Succès", 64) } } LoadDataSimple(content) { global Documents Documents := [] ; Split by ##allobob##DOCUMENT##allobob## blocks blocks := StrSplit(content, "##allobob##DOCUMENT##allobob##") for block in blocks { if (!InStr(block, "##allobob##END##allobob##")) continue ; Extract content between ##allobob##DOCUMENT##allobob## and ##allobob##END##allobob## block := StrSplit(block, "##allobob##END##allobob##")[1] doc := Map() doc.Id := "" doc.Titre := "" doc.Tag := "" doc.ClasseLot := "" doc.DocumentKofax := "" doc.Gouvernement := "" doc.Directives := "" doc.Keywords := "" doc.Filename := "" currentField := "" currentValue := "" lines := StrSplit(block, "`n", "`r") for line in lines { ; Check if it's a field marker (##allobob##FieldName##allobob##) if (RegExMatch(line, "^##allobob##(.+)##allobob##$", &match)) { ; Save previous field if (currentField) { doc.%currentField% := Trim(currentValue, "`n") } ; Start new field currentField := match[1] currentValue := "" } else if (currentField) { ; Add line to current field value if (currentValue != "") currentValue .= "`n" currentValue .= line } } ; Save last field if (currentField) { doc.%currentField% := Trim(currentValue, "`n") } ; Add document if it has an ID if (doc.Id != "") Documents.Push(doc) } } LoadDataSimpleV1(content) { global Documents Documents := [] ; Old ##DOCUMENT## format for backwards compatibility blocks := StrSplit(content, "##DOCUMENT##") for block in blocks { if (!InStr(block, "##END##")) continue ; Extract content between ##DOCUMENT## and ##END## block := StrSplit(block, "##END##")[1] doc := Map() doc.Id := "" doc.Titre := "" doc.Tag := "" doc.ClasseLot := "" doc.DocumentKofax := "" doc.Gouvernement := "" doc.Directives := "" doc.Keywords := "" doc.Filename := "" currentField := "" currentValue := "" lines := StrSplit(block, "`n", "`r") for line in lines { ; Check if it's a field marker (##FieldName##) if (RegExMatch(line, "^##(.+)##$", &match)) { ; Save previous field if (currentField) { doc.%currentField% := Trim(currentValue, "`n") } ; Start new field currentField := match[1] currentValue := "" } else if (currentField) { ; Add line to current field value if (currentValue != "") currentValue .= "`n" currentValue .= line } } ; Save last field if (currentField) { doc.%currentField% := Trim(currentValue, "`n") } ; Add document if it has an ID if (doc.Id != "") Documents.Push(doc) } } LoadDataSimpleOld(content) { global Documents Documents := [] ; Old [DOCUMENT] format for backwards compatibility blocks := StrSplit(content, "[DOCUMENT]") for block in blocks { if (!InStr(block, "[/DOCUMENT]")) continue block := StrSplit(block, "[/DOCUMENT]")[1] doc := Map() doc.Id := "" doc.Titre := "" doc.Tag := "" doc.ClasseLot := "" doc.DocumentKofax := "" doc.Gouvernement := "" doc.Directives := "" doc.Keywords := "" doc.Filename := "" currentField := "" currentValue := "" for line in StrSplit(block, "`n", "`r") { line := Trim(line) if (line = "") continue if (InStr(line, "=") && !currentField) { parts := StrSplit(line, "=", , 2) if (parts.Length >= 2) { field := Trim(parts[1]) value := parts[2] if (field = "Directives" || field = "Keywords") { currentField := field currentValue := value } else { doc.%field% := value } } } else if (currentField) { if (RegExMatch(line, "^(Id|Titre|Tag|ClasseLot|DocumentKofax|Gouvernement|Directives|Keywords|Filename)=")) { doc.%currentField% := currentValue currentField := "" currentValue := "" parts := StrSplit(line, "=", , 2) if (parts.Length >= 2) { field := Trim(parts[1]) value := parts[2] if (field = "Directives" || field = "Keywords") { currentField := field currentValue := value } else { doc.%field% := value } } } else { currentValue .= "`n" . line } } } if (currentField) doc.%currentField% := currentValue if (doc.Id != "") Documents.Push(doc) } } LoadDataLegacy(content) { global Documents Documents := [] for line in StrSplit(content, ["`r`n","`n","`r"]) { if (!Trim(line)) continue parts := StrSplit(line, "|") if (parts.Length < 9) continue doc := Map() doc.Id := parts[1] doc.Titre := parts[2] doc.Tag := parts[3] doc.ClasseLot := parts[4] doc.DocumentKofax := parts[5] doc.Gouvernement := parts[6] doc.Directives := parts[7] doc.Keywords := parts[8] doc.Filename := parts[9] Documents.Push(doc) } } JsonParse(jsonStr) { ; Simple JSON parser for our use case jsonStr := Trim(jsonStr) if (SubStr(jsonStr, 1, 1) = "[") return JsonParseArray(jsonStr) else if (SubStr(jsonStr, 1, 1) = "{") return JsonParseObject(jsonStr) return [] } JsonParseArray(jsonStr) { result := [] jsonStr := Trim(SubStr(jsonStr, 2, StrLen(jsonStr) - 2)) ; Remove [ ] if (jsonStr = "") return result ; Split by objects (simple parser) depth := 0 currentObj := "" for i, char in StrSplit(jsonStr) { if (char = "{") depth++ else if (char = "}") depth-- currentObj .= char if (depth = 0 && char = "}") { result.Push(JsonParseObject(Trim(currentObj))) currentObj := "" } } return result } JsonParseObject(jsonStr) { obj := {} jsonStr := Trim(SubStr(jsonStr, 2, StrLen(jsonStr) - 2)) ; Remove { } if (jsonStr = "") return obj ; Simple key-value parser currentKey := "" currentValue := "" inString := false escaped := false afterColon := false Loop Parse, jsonStr { char := A_LoopField if (escaped) { currentValue .= char escaped := false continue } if (char = "\") { escaped := true continue } if (char = '"') { inString := !inString continue } if (!inString) { if (char = ":") { currentKey := Trim(currentKey) afterColon := true continue } else if (char = "," || A_Index = StrLen(jsonStr)) { if (A_Index = StrLen(jsonStr) && char != ",") currentValue .= char currentValue := Trim(currentValue) ; Unescape string - IMPORTANT: do \\ first to avoid corrupting other escapes currentValue := StrReplace(currentValue, "\\", Chr(1)) ; Temporary placeholder currentValue := StrReplace(currentValue, "\n", "`n") currentValue := StrReplace(currentValue, "\r", "`r") currentValue := StrReplace(currentValue, "\t", "`t") currentValue := StrReplace(currentValue, '\"', '"') currentValue := StrReplace(currentValue, Chr(1), "\") obj.%currentKey% := currentValue currentKey := "" currentValue := "" afterColon := false continue } else if (char = " " || char = "`t" || char = "`n" || char = "`r") { continue } } if (afterColon) currentValue .= char else currentKey .= char } return obj } ; ==================== Jxon - JSON Library for AHK v2 ==================== ; Based on Coco's Jxon library, adapted for reliability Jxon_Load(&src, args*) { key := "", is_key := false stack := [ tree := [] ] next := '"{[01234567890-tfn' pos := 0 while ( (ch := SubStr(src, ++pos, 1)) != "" ) { if InStr(" `t`n`r", ch) continue if !InStr(next, ch, true) { testArr := StrSplit(SubStr(src, pos), "`n") lineNum := ObjLength(testArr) colNum := pos - InStr(src, "`n",, -(StrLen(src)-pos+1)) msg := Format("{}: line {} col {} (char {})" (next == "" ? "Extra data" : "Unexpected char") , lineNum, colNum, pos) throw Error(msg, -1, ch) } obj := stack[1] is_array := (Type(obj) = "Array") if InStr(",:]}", ch) { if (key) is_key := false else if (ch = ",") continue else if (ch = ":") { is_key := true next := '"{[01234567890-tfn' continue } else { stack.RemoveAt(1) next := stack[1]==tree ? "" : is_array ? "," : ",}" continue } } if InStr('`"', ch) { i := pos while (i := InStr(src, '`"',, i+1)) { str := StrReplace(SubStr(src, pos+1, i-pos-1), "\\", "``") if (StrLen(str) - StrLen(StrReplace(str, "``"))) & 1 continue ; Unescape JSON string str := StrReplace(SubStr(src, pos+1, i-pos-1), "\\", Chr(0xFF)) str := StrReplace(str, "\/", "/") str := StrReplace(str, '\"', '"') str := StrReplace(str, "\b", "`b") str := StrReplace(str, "\f", "`f") str := StrReplace(str, "\n", "`n") str := StrReplace(str, "\r", "`r") str := StrReplace(str, "\t", "`t") ; Unicode escapes Loop { if !(j := InStr(str, "\u")) break uni := "0x" SubStr(str, j+2, 4) str := SubStr(str, 1, j-1) . Chr(uni) . SubStr(str, j+6) } str := StrReplace(str, Chr(0xFF), "\") pos := i break } if (!i) throw Error("Missing close quote", -1) if (is_key) { key := str next := ":" continue } } else if InStr("{[", ch) { if (is_key) throw Error("Unexpected " . ch, -1) is_array := (ch = "[") obj := is_array ? [] : Map() if (key) stack[1][key] := obj else stack[1].Push(obj) stack.InsertAt(1, obj) next := is_array ? '"]' : '"},' continue } else { if (is_key) throw Error("Unexpected " . ch, -1) if (ch = "t") str := "true", v := true else if (ch = "f") str := "false", v := false else if (ch = "n") str := "null", v := "" else { i := pos while (i <= StrLen(src) && InStr("01234567890+-.eE", SubStr(src, i, 1))) i++ str := SubStr(src, pos, i-pos) if IsNumber(str) v := str + 0 else throw Error("Invalid number: " . str, -1) pos := i-1 } if (key) obj[key] := v else obj.Push(v) } next := is_array ? ",]" : (is_key ? ":" : ",}") } return tree[1] } ObjLength(obj) { try return obj.Length catch return 0 } Jxon_Dump(obj, indent:="", lvl:=1) { if IsObject(obj) { if (Type(obj) = "Array") { if !obj.Length return "[]" body := "" for k, v in obj { if (body != "") body .= "," body .= "`n" . indent . Jxon_Dump(v, indent . " ", lvl+1) } return "[" . body . "`n" . SubStr(indent, 3) . "]" } else if (Type(obj) = "Map") { if !obj.Count return "{}" body := "" for k, v in obj { if (body != "") body .= "," body .= "`n" . indent . Jxon_Escape(k) . ": " . Jxon_Dump(v, indent . " ", lvl+1) } return "{" . body . "`n" . SubStr(indent, 3) . "}" } ; Fallback for other object types return '""' } else if (obj = "") return '""' else if (IsNumber(obj)) return obj else return Jxon_Escape(obj) } Jxon_Escape(str) { str := StrReplace(str, "\", "\\") str := StrReplace(str, "`t", "\t") str := StrReplace(str, "`r", "\r") str := StrReplace(str, "`n", "\n") str := StrReplace(str, "`b", "\b") str := StrReplace(str, "`f", "\f") str := StrReplace(str, "/", "\/") str := StrReplace(str, '"', '\"') return '"' . str . '"' } ; ============================================================================= ; BACKUP SYSTEM ; ============================================================================= OnMainGuiClose(*) { CheckAndBackup() ; Backup automatique à la fermeture ExitApp } CheckAndBackup() { global SavesDir ; Créer le répertoire saves s'il n'existe pas if (!DirExist(SavesDir)) { try DirCreate(SavesDir) catch { return ; Impossible de créer le répertoire } } ; Trouver la sauvegarde la plus récente lastBackupTime := 0 lastBackupDir := "" Loop Files, SavesDir "\backup_*", "D" { ; Extraire le timestamp du nom du répertoire (format: backup_YYYYMMDD_HHMMSS) if (RegExMatch(A_LoopFileName, "backup_(\d{8})_(\d{6})", &match)) { dateStr := match[1] ; YYYYMMDD timeStr := match[2] ; HHMMSS ; Convertir en timestamp year := SubStr(dateStr, 1, 4) month := SubStr(dateStr, 5, 2) day := SubStr(dateStr, 7, 2) hour := SubStr(timeStr, 1, 2) minute := SubStr(timeStr, 3, 2) second := SubStr(timeStr, 5, 2) backupTime := DateDiff(year month day hour minute second, "19700101000000", "Seconds") if (backupTime > lastBackupTime) { lastBackupTime := backupTime lastBackupDir := A_LoopFileFullPath } } } ; Calculer le temps écoulé depuis la dernière sauvegarde currentTime := DateDiff(A_Now, "19700101000000", "Seconds") elapsedSeconds := currentTime - lastBackupTime ; Si plus d'1 heure (3600 secondes) ou pas de sauvegarde, créer une nouvelle sauvegarde if (elapsedSeconds > 3600 || lastBackupTime = 0) { CreateBackup() CleanupOldBackups() } } CreateBackup() { global SavesDir, DataFile, ListsDir, CounterFile, ImagesDir ; Créer le nom du répertoire avec timestamp timestamp := FormatTime(A_Now, "yyyyMMdd_HHmmss") backupDir := SavesDir "\backup_" timestamp try { ; Créer le répertoire de sauvegarde DirCreate(backupDir) ; Créer le sous-répertoire lists DirCreate(backupDir "\lists") ; Sauvegarder catalogue-data.txt if (FileExist(DataFile)) FileCopy(DataFile, backupDir "\catalogue-data.txt", true) ; Sauvegarder counter.txt if (FileExist(CounterFile)) FileCopy(CounterFile, backupDir "\counter.txt", true) ; Sauvegarder les fichiers de listes if (FileExist(ListsDir "\classes_lot.txt")) FileCopy(ListsDir "\classes_lot.txt", backupDir "\lists\classes_lot.txt", true) if (FileExist(ListsDir "\documents_kofax.txt")) FileCopy(ListsDir "\documents_kofax.txt", backupDir "\lists\documents_kofax.txt", true) if (FileExist(ListsDir "\gouvernements.txt")) FileCopy(ListsDir "\gouvernements.txt", backupDir "\lists\gouvernements.txt", true) ; Zipper le dossier images dans la sauvegarde (images.zip) if (DirExist(ImagesDir)) { ps := "Compress-Archive -Path '" ImagesDir "\\*' -DestinationPath '" backupDir "\\images.zip' -Force" cmd := "powershell -NoProfile -NonInteractive -ExecutionPolicy Bypass -Command " ps try RunWait(cmd, , "Hide") catch { ; Ignorer erreur Powershell } ; Si le zip n'a pas été créé, fallback: copie du dossier images if (!FileExist(backupDir "\\images.zip")) { try DirCopy(ImagesDir, backupDir "\\images", true) } } } catch Error as e { ; Erreur silencieuse - ne pas déranger l'utilisateur return } } CleanupOldBackups() { global SavesDir ; Collecter tous les répertoires de sauvegarde avec leur timestamp backups := [] Loop Files, SavesDir "\backup_*", "D" { if (RegExMatch(A_LoopFileName, "backup_(\d{8})_(\d{6})", &match)) { dateStr := match[1] timeStr := match[2] ; Convertir en timestamp pour tri year := SubStr(dateStr, 1, 4) month := SubStr(dateStr, 5, 2) day := SubStr(dateStr, 7, 2) hour := SubStr(timeStr, 1, 2) minute := SubStr(timeStr, 3, 2) second := SubStr(timeStr, 5, 2) backupTime := DateDiff(year month day hour minute second, "19700101000000", "Seconds") backups.Push({path: A_LoopFileFullPath, time: backupTime}) } } ; Si plus de 10 sauvegardes, supprimer les plus anciennes if (backups.Length > 10) { ; Trier par timestamp (du plus récent au plus ancien) sortedBackups := [] for backup in backups sortedBackups.Push(backup) ; Tri à bulles simple (décroissant par time) Loop sortedBackups.Length - 1 { i := A_Index Loop sortedBackups.Length - i { j := A_Index if (sortedBackups[j].time < sortedBackups[j+1].time) { temp := sortedBackups[j] sortedBackups[j] := sortedBackups[j+1] sortedBackups[j+1] := temp } } } ; Supprimer les sauvegardes au-delà de 10 Loop sortedBackups.Length - 10 { indexToDelete := 10 + A_Index if (indexToDelete <= sortedBackups.Length) { try DirDelete(sortedBackups[indexToDelete].path, true) } } } }