@kortix/sandbox 0.4.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (246) hide show
  1. package/config/customize.sh +143 -0
  2. package/config/kortix-env-setup.sh +25 -0
  3. package/kortix-master/package.json +22 -0
  4. package/kortix-master/src/config.ts +22 -0
  5. package/kortix-master/src/index.ts +44 -0
  6. package/kortix-master/src/routes/env.ts +65 -0
  7. package/kortix-master/src/routes/proxy.ts +108 -0
  8. package/kortix-master/src/routes/update.ts +185 -0
  9. package/kortix-master/src/services/proxy.ts +43 -0
  10. package/kortix-master/src/services/secret-store.ts +156 -0
  11. package/kortix-master/tsconfig.json +14 -0
  12. package/opencode/agents/kortix-browser.md +142 -0
  13. package/opencode/agents/kortix-build.md +62 -0
  14. package/opencode/agents/kortix-explore.md +66 -0
  15. package/opencode/agents/kortix-image-gen.md +33 -0
  16. package/opencode/agents/kortix-main.md +450 -0
  17. package/opencode/agents/kortix-plan.md +100 -0
  18. package/opencode/agents/kortix-research.md +84 -0
  19. package/opencode/agents/kortix-sheets.md +61 -0
  20. package/opencode/agents/kortix-slides.md +64 -0
  21. package/opencode/agents/kortix-web-dev.md +572 -0
  22. package/opencode/commands/email.md +36 -0
  23. package/opencode/commands/init.md +43 -0
  24. package/opencode/commands/journal.md +44 -0
  25. package/opencode/commands/memory-init.md +81 -0
  26. package/opencode/commands/memory-search.md +50 -0
  27. package/opencode/commands/memory-status.md +56 -0
  28. package/opencode/commands/research.md +36 -0
  29. package/opencode/commands/search.md +38 -0
  30. package/opencode/commands/slides.md +32 -0
  31. package/opencode/commands/spreadsheet.md +30 -0
  32. package/opencode/memory.json +37 -0
  33. package/opencode/ocx.jsonc +10 -0
  34. package/opencode/opencode.jsonc +103 -0
  35. package/opencode/package.json +25 -0
  36. package/opencode/patches/apply.sh +19 -0
  37. package/opencode/patches/opencode-pty-spawn.txt +49 -0
  38. package/opencode/plugin/background-agents.ts.disabled +483 -0
  39. package/opencode/plugin/kdco-primitives/get-project-id.ts +172 -0
  40. package/opencode/plugin/kdco-primitives/index.ts +26 -0
  41. package/opencode/plugin/kdco-primitives/log-warn.ts +51 -0
  42. package/opencode/plugin/kdco-primitives/mutex.ts +122 -0
  43. package/opencode/plugin/kdco-primitives/shell.ts +138 -0
  44. package/opencode/plugin/kdco-primitives/temp.ts +36 -0
  45. package/opencode/plugin/kdco-primitives/terminal-detect.ts +34 -0
  46. package/opencode/plugin/kdco-primitives/types.ts +13 -0
  47. package/opencode/plugin/kdco-primitives/with-timeout.ts +84 -0
  48. package/opencode/plugin/memory.ts +306 -0
  49. package/opencode/plugin/worktree/state.ts +412 -0
  50. package/opencode/plugin/worktree/terminal.ts +1002 -0
  51. package/opencode/plugin/worktree.ts +861 -0
  52. package/opencode/skills/KORTIX-browser/SKILL.md +478 -0
  53. package/opencode/skills/KORTIX-cron-triggers/SKILL.md +173 -0
  54. package/opencode/skills/KORTIX-deep-research/SKILL.md +278 -0
  55. package/opencode/skills/KORTIX-docx/SKILL.md +398 -0
  56. package/opencode/skills/KORTIX-docx/scripts/__init__.py +1 -0
  57. package/opencode/skills/KORTIX-docx/scripts/accept_changes.py +104 -0
  58. package/opencode/skills/KORTIX-docx/scripts/comment.py +244 -0
  59. package/opencode/skills/KORTIX-docx/scripts/office/helpers/__init__.py +0 -0
  60. package/opencode/skills/KORTIX-docx/scripts/office/helpers/merge_runs.py +199 -0
  61. package/opencode/skills/KORTIX-docx/scripts/office/helpers/simplify_redlines.py +197 -0
  62. package/opencode/skills/KORTIX-docx/scripts/office/pack.py +159 -0
  63. package/opencode/skills/KORTIX-docx/scripts/office/soffice.py +183 -0
  64. package/opencode/skills/KORTIX-docx/scripts/office/unpack.py +132 -0
  65. package/opencode/skills/KORTIX-docx/scripts/office/validate.py +111 -0
  66. package/opencode/skills/KORTIX-docx/scripts/office/validators/__init__.py +15 -0
  67. package/opencode/skills/KORTIX-docx/scripts/office/validators/base.py +847 -0
  68. package/opencode/skills/KORTIX-docx/scripts/office/validators/docx.py +446 -0
  69. package/opencode/skills/KORTIX-docx/scripts/office/validators/pptx.py +275 -0
  70. package/opencode/skills/KORTIX-docx/scripts/office/validators/redlining.py +247 -0
  71. package/opencode/skills/KORTIX-docx/scripts/render_docx.py +179 -0
  72. package/opencode/skills/KORTIX-docx/scripts/templates/comments.xml +3 -0
  73. package/opencode/skills/KORTIX-docx/scripts/templates/commentsExtended.xml +3 -0
  74. package/opencode/skills/KORTIX-docx/scripts/templates/commentsExtensible.xml +3 -0
  75. package/opencode/skills/KORTIX-docx/scripts/templates/commentsIds.xml +3 -0
  76. package/opencode/skills/KORTIX-docx/scripts/templates/people.xml +3 -0
  77. package/opencode/skills/KORTIX-domain-research/SKILL.md +96 -0
  78. package/opencode/skills/KORTIX-domain-research/scripts/domain-lookup.py +810 -0
  79. package/opencode/skills/KORTIX-elevenlabs/SKILL.md +230 -0
  80. package/opencode/skills/KORTIX-elevenlabs/scripts/tts.py +389 -0
  81. package/opencode/skills/KORTIX-email/SKILL.md +145 -0
  82. package/opencode/skills/KORTIX-legal-writer/SKILL.md +409 -0
  83. package/opencode/skills/KORTIX-legal-writer/references/bluebook.md +152 -0
  84. package/opencode/skills/KORTIX-legal-writer/references/document-types.md +416 -0
  85. package/opencode/skills/KORTIX-legal-writer/scripts/courtlistener.py +291 -0
  86. package/opencode/skills/KORTIX-legal-writer/scripts/ecfr_lookup.py +299 -0
  87. package/opencode/skills/KORTIX-legal-writer/scripts/verify-legal.py +507 -0
  88. package/opencode/skills/KORTIX-logo-creator/SKILL.md +293 -0
  89. package/opencode/skills/KORTIX-logo-creator/references/prompt-patterns.md +134 -0
  90. package/opencode/skills/KORTIX-logo-creator/scripts/compose_logo.py +406 -0
  91. package/opencode/skills/KORTIX-logo-creator/scripts/create_logo_sheet.py +258 -0
  92. package/opencode/skills/KORTIX-logo-creator/scripts/remove_bg.py +96 -0
  93. package/opencode/skills/KORTIX-memory/SKILL.md +261 -0
  94. package/opencode/skills/KORTIX-memory/scripts/export-sessions.py +409 -0
  95. package/opencode/skills/KORTIX-paper-creator/SKILL.md +549 -0
  96. package/opencode/skills/KORTIX-paper-creator/assets/template.tex +101 -0
  97. package/opencode/skills/KORTIX-paper-creator/scripts/compile.sh +177 -0
  98. package/opencode/skills/KORTIX-paper-creator/scripts/openalex_to_bibtex.py +220 -0
  99. package/opencode/skills/KORTIX-paper-creator/scripts/verify.sh +354 -0
  100. package/opencode/skills/KORTIX-paper-search/SKILL.md +418 -0
  101. package/opencode/skills/KORTIX-pdf/SKILL.md +232 -0
  102. package/opencode/skills/KORTIX-pdf/forms.md +36 -0
  103. package/opencode/skills/KORTIX-pdf/reference.md +105 -0
  104. package/opencode/skills/KORTIX-pdf/scripts/check_bounding_boxes.py +65 -0
  105. package/opencode/skills/KORTIX-pdf/scripts/check_fillable_fields.py +11 -0
  106. package/opencode/skills/KORTIX-pdf/scripts/convert_pdf_to_images.py +33 -0
  107. package/opencode/skills/KORTIX-pdf/scripts/create_validation_image.py +37 -0
  108. package/opencode/skills/KORTIX-pdf/scripts/extract_form_field_info.py +122 -0
  109. package/opencode/skills/KORTIX-pdf/scripts/extract_form_structure.py +115 -0
  110. package/opencode/skills/KORTIX-pdf/scripts/fill_fillable_fields.py +98 -0
  111. package/opencode/skills/KORTIX-pdf/scripts/fill_pdf_form_with_annotations.py +107 -0
  112. package/opencode/skills/KORTIX-plan/SKILL.md +228 -0
  113. package/opencode/skills/KORTIX-presentation-viewer/SKILL.md +87 -0
  114. package/opencode/skills/KORTIX-presentation-viewer/serve.ts +136 -0
  115. package/opencode/skills/KORTIX-presentation-viewer/viewer.html +559 -0
  116. package/opencode/skills/KORTIX-presentations/SKILL.md +344 -0
  117. package/opencode/skills/KORTIX-remotion/SKILL.md +56 -0
  118. package/opencode/skills/KORTIX-remotion/rules/3d.md +86 -0
  119. package/opencode/skills/KORTIX-remotion/rules/animations.md +29 -0
  120. package/opencode/skills/KORTIX-remotion/rules/assets.md +78 -0
  121. package/opencode/skills/KORTIX-remotion/rules/audio-visualization.md +198 -0
  122. package/opencode/skills/KORTIX-remotion/rules/audio.md +169 -0
  123. package/opencode/skills/KORTIX-remotion/rules/calculate-metadata.md +104 -0
  124. package/opencode/skills/KORTIX-remotion/rules/can-decode.md +75 -0
  125. package/opencode/skills/KORTIX-remotion/rules/charts.md +120 -0
  126. package/opencode/skills/KORTIX-remotion/rules/compositions.md +141 -0
  127. package/opencode/skills/KORTIX-remotion/rules/display-captions.md +184 -0
  128. package/opencode/skills/KORTIX-remotion/rules/extract-frames.md +229 -0
  129. package/opencode/skills/KORTIX-remotion/rules/ffmpeg.md +38 -0
  130. package/opencode/skills/KORTIX-remotion/rules/fonts.md +152 -0
  131. package/opencode/skills/KORTIX-remotion/rules/get-audio-duration.md +58 -0
  132. package/opencode/skills/KORTIX-remotion/rules/get-video-dimensions.md +68 -0
  133. package/opencode/skills/KORTIX-remotion/rules/get-video-duration.md +58 -0
  134. package/opencode/skills/KORTIX-remotion/rules/gifs.md +141 -0
  135. package/opencode/skills/KORTIX-remotion/rules/images.md +130 -0
  136. package/opencode/skills/KORTIX-remotion/rules/import-srt-captions.md +69 -0
  137. package/opencode/skills/KORTIX-remotion/rules/light-leaks.md +73 -0
  138. package/opencode/skills/KORTIX-remotion/rules/lottie.md +68 -0
  139. package/opencode/skills/KORTIX-remotion/rules/maps.md +401 -0
  140. package/opencode/skills/KORTIX-remotion/rules/measuring-dom-nodes.md +35 -0
  141. package/opencode/skills/KORTIX-remotion/rules/measuring-text.md +143 -0
  142. package/opencode/skills/KORTIX-remotion/rules/parameters.md +98 -0
  143. package/opencode/skills/KORTIX-remotion/rules/sequencing.md +118 -0
  144. package/opencode/skills/KORTIX-remotion/rules/subtitles.md +36 -0
  145. package/opencode/skills/KORTIX-remotion/rules/tailwind.md +11 -0
  146. package/opencode/skills/KORTIX-remotion/rules/text-animations.md +20 -0
  147. package/opencode/skills/KORTIX-remotion/rules/timing.md +179 -0
  148. package/opencode/skills/KORTIX-remotion/rules/transcribe-captions.md +70 -0
  149. package/opencode/skills/KORTIX-remotion/rules/transitions.md +197 -0
  150. package/opencode/skills/KORTIX-remotion/rules/transparent-videos.md +106 -0
  151. package/opencode/skills/KORTIX-remotion/rules/trimming.md +53 -0
  152. package/opencode/skills/KORTIX-remotion/rules/videos.md +171 -0
  153. package/opencode/skills/KORTIX-secrets/SKILL.md +280 -0
  154. package/opencode/skills/KORTIX-semantic-search/SKILL.md +213 -0
  155. package/opencode/skills/KORTIX-session-search/SKILL.md +807 -0
  156. package/opencode/skills/KORTIX-session-search/Untitled +1 -0
  157. package/opencode/skills/KORTIX-skill-creator/SKILL.md +163 -0
  158. package/opencode/skills/KORTIX-web-research/SKILL.md +69 -0
  159. package/opencode/skills/KORTIX-xlsx/LICENSE.txt +30 -0
  160. package/opencode/skills/KORTIX-xlsx/SKILL.md +549 -0
  161. package/opencode/skills/KORTIX-xlsx/scripts/office/helpers/__init__.py +0 -0
  162. package/opencode/skills/KORTIX-xlsx/scripts/office/helpers/merge_runs.py +199 -0
  163. package/opencode/skills/KORTIX-xlsx/scripts/office/helpers/simplify_redlines.py +197 -0
  164. package/opencode/skills/KORTIX-xlsx/scripts/office/pack.py +159 -0
  165. package/opencode/skills/KORTIX-xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/dml-chart.xsd +1499 -0
  166. package/opencode/skills/KORTIX-xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/dml-chartDrawing.xsd +146 -0
  167. package/opencode/skills/KORTIX-xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/dml-diagram.xsd +1085 -0
  168. package/opencode/skills/KORTIX-xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/dml-lockedCanvas.xsd +11 -0
  169. package/opencode/skills/KORTIX-xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/dml-main.xsd +3081 -0
  170. package/opencode/skills/KORTIX-xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/dml-picture.xsd +23 -0
  171. package/opencode/skills/KORTIX-xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/dml-spreadsheetDrawing.xsd +185 -0
  172. package/opencode/skills/KORTIX-xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/dml-wordprocessingDrawing.xsd +287 -0
  173. package/opencode/skills/KORTIX-xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/pml.xsd +1676 -0
  174. package/opencode/skills/KORTIX-xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/shared-additionalCharacteristics.xsd +28 -0
  175. package/opencode/skills/KORTIX-xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/shared-bibliography.xsd +144 -0
  176. package/opencode/skills/KORTIX-xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/shared-commonSimpleTypes.xsd +174 -0
  177. package/opencode/skills/KORTIX-xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/shared-customXmlDataProperties.xsd +25 -0
  178. package/opencode/skills/KORTIX-xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/shared-customXmlSchemaProperties.xsd +18 -0
  179. package/opencode/skills/KORTIX-xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/shared-documentPropertiesCustom.xsd +59 -0
  180. package/opencode/skills/KORTIX-xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/shared-documentPropertiesExtended.xsd +56 -0
  181. package/opencode/skills/KORTIX-xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/shared-documentPropertiesVariantTypes.xsd +195 -0
  182. package/opencode/skills/KORTIX-xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/shared-math.xsd +582 -0
  183. package/opencode/skills/KORTIX-xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/shared-relationshipReference.xsd +25 -0
  184. package/opencode/skills/KORTIX-xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/sml.xsd +4439 -0
  185. package/opencode/skills/KORTIX-xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/vml-main.xsd +570 -0
  186. package/opencode/skills/KORTIX-xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/vml-officeDrawing.xsd +509 -0
  187. package/opencode/skills/KORTIX-xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/vml-presentationDrawing.xsd +12 -0
  188. package/opencode/skills/KORTIX-xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/vml-spreadsheetDrawing.xsd +108 -0
  189. package/opencode/skills/KORTIX-xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/vml-wordprocessingDrawing.xsd +96 -0
  190. package/opencode/skills/KORTIX-xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/wml.xsd +3646 -0
  191. package/opencode/skills/KORTIX-xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/xml.xsd +116 -0
  192. package/opencode/skills/KORTIX-xlsx/scripts/office/schemas/ecma/fouth-edition/opc-contentTypes.xsd +42 -0
  193. package/opencode/skills/KORTIX-xlsx/scripts/office/schemas/ecma/fouth-edition/opc-coreProperties.xsd +50 -0
  194. package/opencode/skills/KORTIX-xlsx/scripts/office/schemas/ecma/fouth-edition/opc-digSig.xsd +49 -0
  195. package/opencode/skills/KORTIX-xlsx/scripts/office/schemas/ecma/fouth-edition/opc-relationships.xsd +33 -0
  196. package/opencode/skills/KORTIX-xlsx/scripts/office/schemas/mce/mc.xsd +75 -0
  197. package/opencode/skills/KORTIX-xlsx/scripts/office/schemas/microsoft/wml-2010.xsd +560 -0
  198. package/opencode/skills/KORTIX-xlsx/scripts/office/schemas/microsoft/wml-2012.xsd +67 -0
  199. package/opencode/skills/KORTIX-xlsx/scripts/office/schemas/microsoft/wml-2018.xsd +14 -0
  200. package/opencode/skills/KORTIX-xlsx/scripts/office/schemas/microsoft/wml-cex-2018.xsd +20 -0
  201. package/opencode/skills/KORTIX-xlsx/scripts/office/schemas/microsoft/wml-cid-2016.xsd +13 -0
  202. package/opencode/skills/KORTIX-xlsx/scripts/office/schemas/microsoft/wml-sdtdatahash-2020.xsd +4 -0
  203. package/opencode/skills/KORTIX-xlsx/scripts/office/schemas/microsoft/wml-symex-2015.xsd +8 -0
  204. package/opencode/skills/KORTIX-xlsx/scripts/office/soffice.py +183 -0
  205. package/opencode/skills/KORTIX-xlsx/scripts/office/unpack.py +132 -0
  206. package/opencode/skills/KORTIX-xlsx/scripts/office/validate.py +111 -0
  207. package/opencode/skills/KORTIX-xlsx/scripts/office/validators/__init__.py +15 -0
  208. package/opencode/skills/KORTIX-xlsx/scripts/office/validators/base.py +847 -0
  209. package/opencode/skills/KORTIX-xlsx/scripts/office/validators/docx.py +446 -0
  210. package/opencode/skills/KORTIX-xlsx/scripts/office/validators/pptx.py +275 -0
  211. package/opencode/skills/KORTIX-xlsx/scripts/office/validators/redlining.py +247 -0
  212. package/opencode/skills/KORTIX-xlsx/scripts/recalc.py +184 -0
  213. package/opencode/tools/image-gen.ts +342 -0
  214. package/opencode/tools/image-search.ts +190 -0
  215. package/opencode/tools/memory-get.ts +168 -0
  216. package/opencode/tools/memory-search.ts +247 -0
  217. package/opencode/tools/presentation-gen.ts +723 -0
  218. package/opencode/tools/scrape-webpage.ts +115 -0
  219. package/opencode/tools/scripts/.python-version +1 -0
  220. package/opencode/tools/scripts/convert_pdf.py +184 -0
  221. package/opencode/tools/scripts/convert_pptx.py +562 -0
  222. package/opencode/tools/scripts/pyproject.toml +11 -0
  223. package/opencode/tools/scripts/uv.lock +287 -0
  224. package/opencode/tools/scripts/validate_slide.py +74 -0
  225. package/opencode/tools/show-user.ts +217 -0
  226. package/opencode/tools/tests/e2e-presentation-fix.ts +277 -0
  227. package/opencode/tools/tests/image-gen.test.ts +215 -0
  228. package/opencode/tools/tests/image-search.test.ts +125 -0
  229. package/opencode/tools/tests/memory-system-benchmark.ts +1076 -0
  230. package/opencode/tools/tests/presentation-gen.test.ts +389 -0
  231. package/opencode/tools/tests/scrape-webpage.test.ts +74 -0
  232. package/opencode/tools/tests/show-user.test.ts +241 -0
  233. package/opencode/tools/tests/video-gen.test.ts +110 -0
  234. package/opencode/tools/tests/web-search.test.ts +106 -0
  235. package/opencode/tools/video-gen.ts +200 -0
  236. package/opencode/tools/web-search.ts +153 -0
  237. package/opencode/tsconfig.json +29 -0
  238. package/package.json +36 -0
  239. package/patch-agent-browser.js +100 -0
  240. package/postinstall.sh +88 -0
  241. package/services/KORTIX-presentation-viewer/run +37 -0
  242. package/services/agent-browser-viewer/run +48 -0
  243. package/services/kortix-master/run +16 -0
  244. package/services/lss-sync/run +22 -0
  245. package/services/opencode-serve/run +25 -0
  246. package/services/opencode-web/run +21 -0
@@ -0,0 +1,406 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ Compose logos by combining symbol images with Google Fonts text.
4
+
5
+ Renders HTML compositions to PNG via Playwright. Produces:
6
+ - Logomark (symbol only, cleaned up)
7
+ - Wordmark (text only, styled with Google Font)
8
+ - Combination marks (symbol + text in various layouts)
9
+
10
+ Usage:
11
+ python3 compose_logo.py <config_json>
12
+
13
+ Config JSON format:
14
+ {
15
+ "brand_name": "Acme",
16
+ "symbol_path": "logos/acme/round-1/logomark-arrow.png",
17
+ "output_dir": "logos/acme/composed/",
18
+ "font_family": "Inter",
19
+ "font_weight": 700,
20
+ "text_color": "#000000",
21
+ "accent_color": "#3B82F6",
22
+ "tagline": "Build the future",
23
+ "letter_spacing": "0.02em",
24
+ "text_transform": "none",
25
+ "layouts": ["wordmark", "combo-horizontal", "combo-vertical", "combo-icon-right"],
26
+ "background": "#ffffff"
27
+ }
28
+
29
+ CLI mode:
30
+ python3 compose_logo.py --brand "Acme" --symbol path.png --output-dir out/ \
31
+ --font "Space Grotesk" --weight 700 --color "#1a1a2e" --layouts all
32
+ """
33
+
34
+ import sys
35
+ import os
36
+ import json
37
+ import argparse
38
+ import tempfile
39
+ import base64
40
+ from pathlib import Path
41
+
42
+ try:
43
+ from playwright.sync_api import sync_playwright
44
+ except ImportError:
45
+ print("Error: playwright required. Install: pip install playwright && playwright install chromium")
46
+ sys.exit(1)
47
+
48
+ try:
49
+ from PIL import Image
50
+ import numpy as np
51
+ HAS_PIL = True
52
+ except ImportError:
53
+ HAS_PIL = False
54
+
55
+
56
+ # Layout dimensions (width x height) — designed for good proportions at each layout
57
+ LAYOUT_SIZES = {
58
+ "wordmark": (1600, 400),
59
+ "wordmark-tagline": (1600, 500),
60
+ "combo-horizontal": (2000, 600),
61
+ "combo-vertical": (1000, 1200),
62
+ "combo-icon-right": (2000, 600),
63
+ "logomark": (800, 800),
64
+ }
65
+
66
+ ALL_LAYOUTS = ["logomark", "wordmark", "wordmark-tagline", "combo-horizontal", "combo-vertical", "combo-icon-right"]
67
+
68
+
69
+ def autocrop_symbol(path: str, padding_pct: float = 0.08) -> str:
70
+ """Auto-crop whitespace/transparency from a symbol image.
71
+
72
+ Returns path to cropped image (saves as -cropped.png next to original).
73
+ The cropped image is padded by padding_pct of the content size and
74
+ placed in a square canvas.
75
+ """
76
+ if not HAS_PIL:
77
+ print(" Warning: PIL not available, skipping auto-crop")
78
+ return path
79
+
80
+ img = Image.open(path).convert("RGBA")
81
+ arr = np.array(img)
82
+
83
+ # Find non-transparent, non-white pixels
84
+ if arr.shape[2] == 4:
85
+ # Has alpha — use alpha channel
86
+ mask = arr[:, :, 3] > 20 # not nearly transparent
87
+ else:
88
+ # No alpha — find non-white
89
+ mask = (arr[:, :, 0] < 240) | (arr[:, :, 1] < 240) | (arr[:, :, 2] < 240)
90
+
91
+ rows = np.any(mask, axis=1)
92
+ cols = np.any(mask, axis=0)
93
+
94
+ if not np.any(rows) or not np.any(cols):
95
+ return path
96
+
97
+ rmin, rmax = np.where(rows)[0][[0, -1]]
98
+ cmin, cmax = np.where(cols)[0][[0, -1]]
99
+
100
+ # Crop to content
101
+ cropped = img.crop((cmin, rmin, cmax + 1, rmax + 1))
102
+
103
+ # Add proportional padding and make square
104
+ cw, ch = cropped.size
105
+ padding = int(max(cw, ch) * padding_pct)
106
+ size = max(cw, ch) + padding * 2
107
+
108
+ # Create square canvas (transparent)
109
+ square = Image.new("RGBA", (size, size), (0, 0, 0, 0))
110
+ x = (size - cw) // 2
111
+ y = (size - ch) // 2
112
+ square.paste(cropped, (x, y))
113
+
114
+ out_path = str(Path(path).with_suffix("")) + "-cropped.png"
115
+ square.save(out_path, "PNG")
116
+ print(f" Auto-cropped symbol: {img.size[0]}x{img.size[1]} -> {size}x{size} (content {cw}x{ch})")
117
+ return out_path
118
+
119
+
120
+ def symbol_to_data_uri(path: str) -> str:
121
+ """Convert symbol image to base64 data URI."""
122
+ ext = Path(path).suffix.lower()
123
+ mime = {".png": "image/png", ".jpg": "image/jpeg", ".jpeg": "image/jpeg",
124
+ ".webp": "image/webp", ".svg": "image/svg+xml"}.get(ext, "image/png")
125
+ with open(path, "rb") as f:
126
+ return f"data:{mime};base64,{base64.b64encode(f.read()).decode('ascii')}"
127
+
128
+
129
+ def build_html(config: dict, layout: str) -> str:
130
+ """Build HTML for a specific logo layout."""
131
+ brand = config["brand_name"]
132
+ font = config.get("font_family", "Inter")
133
+ weight = config.get("font_weight", 700)
134
+ color = config.get("text_color", "#000000")
135
+ tagline = config.get("tagline", "")
136
+ letter_spacing = config.get("letter_spacing", "0.02em")
137
+ text_transform = config.get("text_transform", "none")
138
+ bg = config.get("background", "#ffffff")
139
+ symbol_uri = ""
140
+ if config.get("symbol_path") and os.path.exists(config["symbol_path"]):
141
+ symbol_uri = symbol_to_data_uri(config["symbol_path"])
142
+
143
+ w, h = config.get("sizes", {}).get(layout, LAYOUT_SIZES.get(layout, (1200, 400)))
144
+
145
+ # Google Fonts import — request the specific weight + 400 for tagline
146
+ font_url_name = font.replace(" ", "+")
147
+ weights = sorted(set([400, weight]))
148
+ weight_str = ";".join(str(w) for w in weights)
149
+ font_import = f'@import url("https://fonts.googleapis.com/css2?family={font_url_name}:wght@{weight_str}&display=swap");'
150
+
151
+ # --- Proportion system ---
152
+ # These are tuned to produce visually balanced logos.
153
+
154
+ if layout == "logomark":
155
+ symbol_sz = int(h * 0.65)
156
+ inner = f'''
157
+ <div class="center">
158
+ <img src="{symbol_uri}" style="width:{symbol_sz}px;height:{symbol_sz}px;object-fit:contain;" />
159
+ </div>
160
+ '''
161
+
162
+ elif layout == "wordmark":
163
+ fsize = int(h * 0.30)
164
+ inner = f'''
165
+ <div class="center">
166
+ <span class="brand" style="font-size:{fsize}px;">{brand}</span>
167
+ </div>
168
+ '''
169
+
170
+ elif layout == "wordmark-tagline":
171
+ fsize = int(h * 0.26)
172
+ tag_size = int(fsize * 0.32)
173
+ gap = int(fsize * 0.25)
174
+ tag_text = tagline if tagline else "Your tagline here"
175
+ inner = f'''
176
+ <div class="center" style="flex-direction:column;gap:{gap}px;">
177
+ <span class="brand" style="font-size:{fsize}px;">{brand}</span>
178
+ <span class="tagline" style="font-size:{tag_size}px;">{tag_text}</span>
179
+ </div>
180
+ '''
181
+
182
+ elif layout == "combo-horizontal":
183
+ # Symbol is the hero. Text complements it.
184
+ symbol_sz = int(h * 0.70)
185
+ fsize = int(symbol_sz * 0.40)
186
+ tag_size = int(fsize * 0.32)
187
+ gap = int(symbol_sz * 0.20)
188
+ tag_mt = int(tag_size * 0.25)
189
+ inner = f'''
190
+ <div class="center" style="gap:{gap}px;">
191
+ <img src="{symbol_uri}" style="width:{symbol_sz}px;height:{symbol_sz}px;object-fit:contain;" />
192
+ <div style="display:flex;flex-direction:column;justify-content:center;">
193
+ <span class="brand" style="font-size:{fsize}px;line-height:1.05;">{brand}</span>
194
+ {'<span class="tagline" style="font-size:' + str(tag_size) + 'px;margin-top:' + str(tag_mt) + 'px;">' + tagline + '</span>' if tagline else ''}
195
+ </div>
196
+ </div>
197
+ '''
198
+
199
+ elif layout == "combo-vertical":
200
+ symbol_sz = int(h * 0.42)
201
+ fsize = int(symbol_sz * 0.35)
202
+ tag_size = int(fsize * 0.34)
203
+ gap = int(symbol_sz * 0.12)
204
+ inner = f'''
205
+ <div class="center" style="flex-direction:column;gap:{gap}px;">
206
+ <img src="{symbol_uri}" style="width:{symbol_sz}px;height:{symbol_sz}px;object-fit:contain;" />
207
+ <span class="brand" style="font-size:{fsize}px;">{brand}</span>
208
+ {'<span class="tagline" style="font-size:' + str(tag_size) + 'px;">' + tagline + '</span>' if tagline else ''}
209
+ </div>
210
+ '''
211
+
212
+ elif layout == "combo-icon-right":
213
+ symbol_sz = int(h * 0.70)
214
+ fsize = int(symbol_sz * 0.40)
215
+ tag_size = int(fsize * 0.32)
216
+ gap = int(symbol_sz * 0.20)
217
+ tag_mt = int(tag_size * 0.25)
218
+ inner = f'''
219
+ <div class="center" style="gap:{gap}px;">
220
+ <div style="display:flex;flex-direction:column;justify-content:center;">
221
+ <span class="brand" style="font-size:{fsize}px;line-height:1.05;">{brand}</span>
222
+ {'<span class="tagline" style="font-size:' + str(tag_size) + 'px;margin-top:' + str(tag_mt) + 'px;">' + tagline + '</span>' if tagline else ''}
223
+ </div>
224
+ <img src="{symbol_uri}" style="width:{symbol_sz}px;height:{symbol_sz}px;object-fit:contain;" />
225
+ </div>
226
+ '''
227
+ else:
228
+ raise ValueError(f"Unknown layout: {layout}")
229
+
230
+ return f'''<!DOCTYPE html>
231
+ <html>
232
+ <head>
233
+ <meta charset="utf-8"/>
234
+ <style>
235
+ {font_import}
236
+ * {{ margin:0; padding:0; box-sizing:border-box; }}
237
+ body {{
238
+ width:{w}px;
239
+ height:{h}px;
240
+ background:{bg};
241
+ overflow:hidden;
242
+ display:flex;
243
+ align-items:center;
244
+ justify-content:center;
245
+ }}
246
+ .center {{
247
+ display:flex;
248
+ align-items:center;
249
+ justify-content:center;
250
+ width:100%;
251
+ height:100%;
252
+ }}
253
+ .brand {{
254
+ font-family:'{font}',sans-serif;
255
+ font-weight:{weight};
256
+ color:{color};
257
+ letter-spacing:{letter_spacing};
258
+ text-transform:{text_transform};
259
+ white-space:nowrap;
260
+ }}
261
+ .tagline {{
262
+ font-family:'{font}',sans-serif;
263
+ font-weight:400;
264
+ color:{color};
265
+ opacity:0.5;
266
+ letter-spacing:0.1em;
267
+ text-transform:uppercase;
268
+ white-space:nowrap;
269
+ }}
270
+ </style>
271
+ </head>
272
+ <body>
273
+ {inner}
274
+ </body>
275
+ </html>'''
276
+
277
+
278
+ def render_all_to_png(layouts_html: list[tuple[str, str, int, int]]):
279
+ """Render multiple HTML layouts to PNG using a single Playwright browser instance.
280
+
281
+ layouts_html: list of (html_content, output_path, width, height)
282
+ """
283
+ with sync_playwright() as p:
284
+ browser = p.chromium.launch()
285
+ for html, outpath, w, h in layouts_html:
286
+ with tempfile.NamedTemporaryFile(suffix=".html", delete=False, mode="w") as f:
287
+ f.write(html)
288
+ tmp_html = f.name
289
+ try:
290
+ page = browser.new_page(viewport={"width": w, "height": h})
291
+ page.goto(f"file://{tmp_html}")
292
+ page.wait_for_timeout(1500)
293
+ page.screenshot(path=outpath, type="png")
294
+ page.close()
295
+ finally:
296
+ os.unlink(tmp_html)
297
+ browser.close()
298
+
299
+
300
+ def compose_all(config: dict) -> list[str]:
301
+ """Compose all requested layouts and return list of output paths."""
302
+ output_dir = config.get("output_dir", "logos/composed/")
303
+ os.makedirs(output_dir, exist_ok=True)
304
+
305
+ layouts = config.get("layouts", ALL_LAYOUTS)
306
+ if layouts == "all" or layouts == ["all"]:
307
+ layouts = list(ALL_LAYOUTS)
308
+
309
+ # Skip tagline layout if no tagline
310
+ if not config.get("tagline") and "wordmark-tagline" in layouts:
311
+ layouts = [l for l in layouts if l != "wordmark-tagline"]
312
+
313
+ # Auto-crop symbol to remove whitespace padding from AI-generated images
314
+ has_symbol = config.get("symbol_path") and os.path.exists(config.get("symbol_path", ""))
315
+ if has_symbol:
316
+ cropped = autocrop_symbol(config["symbol_path"])
317
+ config = {**config, "symbol_path": cropped}
318
+ has_symbol = True
319
+
320
+ if not has_symbol:
321
+ symbol_layouts = {"logomark", "combo-horizontal", "combo-vertical", "combo-icon-right"}
322
+ skipped = [l for l in layouts if l in symbol_layouts]
323
+ layouts = [l for l in layouts if l not in symbol_layouts]
324
+ if skipped:
325
+ print(f" Skipping layouts (no symbol): {', '.join(skipped)}")
326
+
327
+ brand_slug = config["brand_name"].lower().replace(" ", "-")
328
+
329
+ # Build all HTML and queue for rendering
330
+ render_queue = []
331
+ output_paths = []
332
+
333
+ for layout in layouts:
334
+ w, h = config.get("sizes", {}).get(layout, LAYOUT_SIZES.get(layout, (1200, 400)))
335
+ html = build_html(config, layout)
336
+ filename = f"{brand_slug}-{layout}.png"
337
+ outpath = os.path.join(output_dir, filename)
338
+ render_queue.append((html, outpath, w, h))
339
+ output_paths.append((layout, outpath, w, h))
340
+ print(f" Queued {layout} ({w}x{h})")
341
+
342
+ print(f" Rendering {len(render_queue)} layouts...", flush=True)
343
+ render_all_to_png(render_queue)
344
+
345
+ for layout, outpath, w, h in output_paths:
346
+ size_kb = os.path.getsize(outpath) / 1024
347
+ print(f" OK {layout} -> {outpath} ({size_kb:.0f}KB)")
348
+
349
+ return [p for _, p, _, _ in output_paths]
350
+
351
+
352
+ def main():
353
+ parser = argparse.ArgumentParser(description="Compose logo layouts from symbol + Google Fonts text.")
354
+ parser.add_argument("config_or_brand", nargs="?", help="Path to config JSON, or brand name with --flags")
355
+ parser.add_argument("--brand", help="Brand name")
356
+ parser.add_argument("--symbol", help="Path to symbol/logomark image")
357
+ parser.add_argument("--output-dir", default="logos/composed/", help="Output directory")
358
+ parser.add_argument("--font", default="Inter", help="Google Font family name")
359
+ parser.add_argument("--weight", type=int, default=700, help="Font weight")
360
+ parser.add_argument("--color", default="#000000", help="Text color")
361
+ parser.add_argument("--accent", default=None, help="Accent color")
362
+ parser.add_argument("--bg", default="#ffffff", help="Background color")
363
+ parser.add_argument("--tagline", default="", help="Optional tagline")
364
+ parser.add_argument("--letter-spacing", default="0.02em", help="Letter spacing")
365
+ parser.add_argument("--text-transform", default="none", choices=["none", "uppercase", "lowercase"])
366
+ parser.add_argument("--layouts", default="all", help="Comma-separated layouts or 'all'")
367
+ args = parser.parse_args()
368
+
369
+ if args.config_or_brand and os.path.isfile(args.config_or_brand):
370
+ with open(args.config_or_brand) as f:
371
+ config = json.load(f)
372
+ elif args.brand or args.config_or_brand:
373
+ brand = args.brand or args.config_or_brand
374
+ layouts = args.layouts.split(",") if args.layouts != "all" else "all"
375
+ config = {
376
+ "brand_name": brand,
377
+ "symbol_path": args.symbol or "",
378
+ "output_dir": args.output_dir,
379
+ "font_family": args.font,
380
+ "font_weight": args.weight,
381
+ "text_color": args.color,
382
+ "accent_color": args.accent or args.color,
383
+ "background": args.bg,
384
+ "tagline": args.tagline,
385
+ "letter_spacing": args.letter_spacing,
386
+ "text_transform": args.text_transform,
387
+ "layouts": layouts,
388
+ }
389
+ else:
390
+ parser.print_help()
391
+ sys.exit(1)
392
+
393
+ print(f"Composing logos for '{config['brand_name']}'")
394
+ print(f" Font: {config.get('font_family', 'Inter')} @ {config.get('font_weight', 700)}")
395
+ print(f" Color: {config.get('text_color')} on {config.get('background', '#ffffff')}")
396
+ print(f" Symbol: {config.get('symbol_path') or '(none)'}")
397
+ print(f" Output: {config.get('output_dir')}")
398
+ print()
399
+
400
+ outputs = compose_all(config)
401
+ print(f"\nDone! {len(outputs)} compositions created.")
402
+ return outputs
403
+
404
+
405
+ if __name__ == "__main__":
406
+ main()
@@ -0,0 +1,258 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ Create an HTML contact sheet for comparing logo variations side by side.
4
+
5
+ Usage:
6
+ python3 create_logo_sheet.py <image_dir> <output_html> [--title "Sheet Title"] [--cols 3]
7
+
8
+ Scans <image_dir> for .png/.jpg/.webp files, generates a self-contained HTML
9
+ page with a dark grid layout, modal zoom on click, and light/dark background
10
+ toggle per logo.
11
+
12
+ Example:
13
+ python3 create_logo_sheet.py ./logos ./logos/sheet.html --title "Acme Logomarks Round 1"
14
+ """
15
+
16
+ import sys
17
+ import os
18
+ import base64
19
+ import argparse
20
+ from pathlib import Path
21
+
22
+
23
+ def image_to_data_uri(path: str) -> str:
24
+ """Convert image file to base64 data URI for self-contained HTML."""
25
+ ext = Path(path).suffix.lower()
26
+ mime = {
27
+ ".png": "image/png",
28
+ ".jpg": "image/jpeg",
29
+ ".jpeg": "image/jpeg",
30
+ ".webp": "image/webp",
31
+ ".gif": "image/gif",
32
+ ".svg": "image/svg+xml",
33
+ }.get(ext, "image/png")
34
+ with open(path, "rb") as f:
35
+ data = base64.b64encode(f.read()).decode("ascii")
36
+ return f"data:{mime};base64,{data}"
37
+
38
+
39
+ def find_images(directory: str) -> list[str]:
40
+ """Find all image files in directory, sorted by name."""
41
+ exts = {".png", ".jpg", ".jpeg", ".webp", ".gif", ".svg"}
42
+ images = []
43
+ for entry in sorted(os.listdir(directory)):
44
+ if Path(entry).suffix.lower() in exts:
45
+ full = os.path.join(directory, entry)
46
+ if os.path.isfile(full):
47
+ images.append(full)
48
+ return images
49
+
50
+
51
+ def generate_html(images: list[str], title: str, cols: int) -> str:
52
+ """Generate self-contained HTML contact sheet."""
53
+
54
+ cards = []
55
+ for i, img_path in enumerate(images):
56
+ name = Path(img_path).stem
57
+ data_uri = image_to_data_uri(img_path)
58
+ cards.append(
59
+ f"""
60
+ <div class="card" onclick="openModal({i})">
61
+ <div class="img-wrap" id="wrap-{i}">
62
+ <img src="{data_uri}" alt="{name}" draggable="false" />
63
+ </div>
64
+ <div class="card-footer">
65
+ <span class="card-name">{name}</span>
66
+ <button class="bg-toggle" onclick="event.stopPropagation(); toggleBg({i})" title="Toggle background">
67
+ &#x25D1;
68
+ </button>
69
+ </div>
70
+ </div>"""
71
+ )
72
+
73
+ modal_data = []
74
+ for i, img_path in enumerate(images):
75
+ data_uri = image_to_data_uri(img_path)
76
+ name = Path(img_path).stem
77
+ modal_data.append(f'{{src:"{data_uri}",name:"{name}"}}')
78
+
79
+ return f"""<!DOCTYPE html>
80
+ <html lang="en">
81
+ <head>
82
+ <meta charset="utf-8"/>
83
+ <meta name="viewport" content="width=device-width, initial-scale=1"/>
84
+ <title>{title}</title>
85
+ <style>
86
+ * {{ margin:0; padding:0; box-sizing:border-box; }}
87
+ body {{ background:#111; color:#eee; font-family:'Inter','Segoe UI',system-ui,sans-serif; padding:30px; }}
88
+ h1 {{ font-size:28px; font-weight:700; margin-bottom:8px; }}
89
+ .meta {{ font-size:14px; color:#888; margin-bottom:24px; }}
90
+ .grid {{
91
+ display:grid;
92
+ grid-template-columns:repeat({cols}, 1fr);
93
+ gap:16px;
94
+ }}
95
+ .card {{
96
+ border:1px solid #333; border-radius:12px; overflow:hidden;
97
+ cursor:pointer; transition:border-color .2s, transform .15s;
98
+ background:#1a1a1a;
99
+ }}
100
+ .card:hover {{ border-color:#666; transform:translateY(-2px); }}
101
+ .img-wrap {{
102
+ aspect-ratio:1; display:flex; align-items:center; justify-content:center;
103
+ padding:20px; background:#fff; transition:background .2s;
104
+ }}
105
+ .img-wrap.dark {{ background:#1a1a1a; }}
106
+ .img-wrap img {{ max-width:100%; max-height:100%; object-fit:contain; }}
107
+ .card-footer {{
108
+ display:flex; align-items:center; justify-content:space-between;
109
+ padding:10px 14px; font-size:13px; color:#aaa;
110
+ }}
111
+ .card-name {{ overflow:hidden; text-overflow:ellipsis; white-space:nowrap; max-width:80%; }}
112
+ .bg-toggle {{
113
+ background:none; border:1px solid #555; color:#aaa; border-radius:6px;
114
+ width:28px; height:28px; cursor:pointer; font-size:16px; line-height:1;
115
+ display:flex; align-items:center; justify-content:center; transition:border-color .2s;
116
+ }}
117
+ .bg-toggle:hover {{ border-color:#aaa; color:#fff; }}
118
+
119
+ /* Modal */
120
+ .modal-overlay {{
121
+ display:none; position:fixed; inset:0; background:rgba(0,0,0,.85);
122
+ z-index:1000; align-items:center; justify-content:center;
123
+ }}
124
+ .modal-overlay.open {{ display:flex; }}
125
+ .modal-content {{
126
+ position:relative; max-width:80vw; max-height:80vh;
127
+ display:flex; flex-direction:column; align-items:center;
128
+ }}
129
+ .modal-content img {{ max-width:80vw; max-height:72vh; object-fit:contain; border-radius:8px; }}
130
+ .modal-name {{ margin-top:12px; font-size:16px; color:#ccc; }}
131
+ .modal-close {{
132
+ position:fixed; top:20px; right:30px; font-size:36px; color:#888;
133
+ cursor:pointer; z-index:1001; line-height:1; background:none; border:none;
134
+ }}
135
+ .modal-close:hover {{ color:#fff; }}
136
+ .modal-nav {{
137
+ position:fixed; top:50%; font-size:48px; color:#666; cursor:pointer;
138
+ z-index:1001; background:none; border:none; transform:translateY(-50%);
139
+ padding:10px; transition:color .2s;
140
+ }}
141
+ .modal-nav:hover {{ color:#fff; }}
142
+ .modal-prev {{ left:20px; }}
143
+ .modal-next {{ right:20px; }}
144
+ .modal-bg-toggle {{
145
+ position:fixed; bottom:30px; left:50%; transform:translateX(-50%);
146
+ background:#333; border:1px solid #555; color:#ccc; border-radius:8px;
147
+ padding:8px 20px; font-size:14px; cursor:pointer; z-index:1001;
148
+ }}
149
+ .modal-bg-toggle:hover {{ background:#444; color:#fff; }}
150
+ .modal-img-wrap {{
151
+ background:#fff; border-radius:8px; padding:30px; transition:background .2s;
152
+ display:flex; align-items:center; justify-content:center;
153
+ }}
154
+ .modal-img-wrap.dark {{ background:#1a1a1a; }}
155
+
156
+ /* Counter badge */
157
+ .counter {{
158
+ position:fixed; bottom:30px; right:30px; background:#333; border:1px solid #555;
159
+ border-radius:8px; padding:6px 14px; font-size:13px; color:#888; z-index:1001;
160
+ display:none;
161
+ }}
162
+ .modal-overlay.open ~ .counter {{ display:block; }}
163
+ </style>
164
+ </head>
165
+ <body>
166
+ <h1>{title}</h1>
167
+ <div class="meta">{len(images)} variations</div>
168
+ <div class="grid">
169
+ {''.join(cards)}
170
+ </div>
171
+
172
+ <div class="modal-overlay" id="modal">
173
+ <button class="modal-close" onclick="closeModal()">&times;</button>
174
+ <button class="modal-nav modal-prev" onclick="navModal(-1)">&#8249;</button>
175
+ <button class="modal-nav modal-next" onclick="navModal(1)">&#8250;</button>
176
+ <div class="modal-content">
177
+ <div class="modal-img-wrap" id="modal-img-wrap">
178
+ <img id="modal-img" src="" alt="" />
179
+ </div>
180
+ <div class="modal-name" id="modal-name"></div>
181
+ </div>
182
+ <button class="modal-bg-toggle" onclick="toggleModalBg()">Toggle Background</button>
183
+ </div>
184
+ <div class="counter" id="counter"></div>
185
+
186
+ <script>
187
+ const data = [{','.join(modal_data)}];
188
+ let current = 0;
189
+
190
+ function openModal(i) {{
191
+ current = i;
192
+ showModal();
193
+ }}
194
+ function showModal() {{
195
+ const m = document.getElementById('modal');
196
+ document.getElementById('modal-img').src = data[current].src;
197
+ document.getElementById('modal-name').textContent = data[current].name;
198
+ document.getElementById('counter').textContent = (current+1) + ' / ' + data.length;
199
+ m.classList.add('open');
200
+ document.getElementById('counter').style.display = 'block';
201
+ }}
202
+ function closeModal() {{
203
+ document.getElementById('modal').classList.remove('open');
204
+ document.getElementById('counter').style.display = 'none';
205
+ }}
206
+ function navModal(dir) {{
207
+ current = (current + dir + data.length) % data.length;
208
+ showModal();
209
+ }}
210
+ function toggleBg(i) {{
211
+ document.getElementById('wrap-' + i).classList.toggle('dark');
212
+ }}
213
+ function toggleModalBg() {{
214
+ document.getElementById('modal-img-wrap').classList.toggle('dark');
215
+ }}
216
+ document.addEventListener('keydown', e => {{
217
+ const m = document.getElementById('modal');
218
+ if (!m.classList.contains('open')) return;
219
+ if (e.key === 'Escape') closeModal();
220
+ if (e.key === 'ArrowLeft') navModal(-1);
221
+ if (e.key === 'ArrowRight') navModal(1);
222
+ if (e.key === 'b' || e.key === 'B') toggleModalBg();
223
+ }});
224
+ </script>
225
+ </body>
226
+ </html>"""
227
+
228
+
229
+ def main():
230
+ parser = argparse.ArgumentParser(description="Generate an HTML logo contact sheet.")
231
+ parser.add_argument("image_dir", help="Directory containing logo images")
232
+ parser.add_argument("output_html", help="Path for the output HTML file")
233
+ parser.add_argument("--title", default="Logo Variations", help="Sheet title")
234
+ parser.add_argument("--cols", type=int, default=3, help="Grid columns (default: 3)")
235
+ args = parser.parse_args()
236
+
237
+ if not os.path.isdir(args.image_dir):
238
+ print(f"Error: '{args.image_dir}' is not a directory")
239
+ sys.exit(1)
240
+
241
+ images = find_images(args.image_dir)
242
+ if not images:
243
+ print(f"Error: No images found in '{args.image_dir}'")
244
+ sys.exit(1)
245
+
246
+ html = generate_html(images, args.title, args.cols)
247
+
248
+ os.makedirs(os.path.dirname(os.path.abspath(args.output_html)), exist_ok=True)
249
+ with open(args.output_html, "w") as f:
250
+ f.write(html)
251
+
252
+ print(f"Created contact sheet: {args.output_html}")
253
+ print(f" Images: {len(images)}")
254
+ print(f" Columns: {args.cols}")
255
+
256
+
257
+ if __name__ == "__main__":
258
+ main()